将 Gemini Enterprise 智能体与 Google Workspace 集成

1. 准备工作

83e1c1629d14fb31.png

什么是 Gemini Enterprise?

Gemini Enterprise 是一个先进的智能体平台,它将 Google AI 的卓越能力带给每一位员工,融入每一个工作流。它使团队能够在同一安全环境中发现、创建、共享和运行 AI 智能体。

  • 使用高级模型:用户可以立即使用 Google 最强大的多模态 AI(包括 Gemini)来应对复杂的业务挑战。
  • 利用专业智能体:该套件包含可直接使用的 Google 智能体,可用于研究、编码和记笔记,立即创造价值。
  • 赋能每一位员工:通过无代码和专业代码选项,各部门的员工都可以构建和管理自己的自定义智能体,以实现工作流自动化。
  • 让代理基于数据运行:代理可以安全地连接到公司内部数据和第三方应用,以确保其回答在上下文中准确无误。
  • 集中式治理:管理员可以直观呈现和审核所有智能体活动,确保组织符合严格的安全和合规性标准。
  • 通过生态系统进行扩展:该平台与广泛的合作伙伴应用和服务提供商网络集成,可在不同系统之间扩展自动化功能。

127f2ed7d484722c.png

什么是 Google Workspace?

Google Workspace 是一套基于云的高效办公和协作解决方案,专为个人、学校和企业打造:

  • 沟通:专业电子邮件服务 (Gmail)、视频会议 (Meet) 和团队消息服务 (Chat)。
  • 内容创作:用于撰写文档 (Google 文档)、构建电子表格 (Google 表格) 和设计演示文稿 (Google 幻灯片) 的工具。
  • 组织:共享日历(日历)和数字记事(Keep)。
  • 存储空间:用于安全地保存和共享文件的集中式云空间(云端硬盘)。
  • 管理:用于管理用户和安全设置的管理控件(Workspace 管理控制台)。

哪些类型的自定义集成?

Google Workspace 和 Gemini Enterprise 形成强大的反馈环路,其中 Workspace 提供实时数据和协作上下文,而 Gemini Enterprise 提供自动执行智能工作流所需的模型、智能体推理和编排功能。

  • 智能连接:Google 管理的数据存储区、API 和 MCP 服务器(Google 管理的服务器和自定义服务器)可让代理安全无缝地访问 Workspace 数据,并代表用户执行操作。
  • 自定义代理:团队可以使用无代码设计器或专业代码框架,基于管理员管控的 Workspace 数据和操作构建专业代理。
  • 原生集成:无论是通过专用界面组件还是后台进程,Workspace 加购项都能弥合 AI 系统与 Chat 和 Gmail 等应用之间的差距。这样,客服人员就可以在用户需要时立即提供贴合情境的帮助。

通过将 Google Workspace 强大的效率生态系统与 Gemini Enterprise 先进的智能体功能相结合,组织可以借助自定义的、以数据为基础的 AI 智能体来转变运营方式,直接在团队日常使用的工具中自动执行复杂的工作流程。

前提条件

如果您想在自己的环境中完成所有步骤,则需要:

构建内容

在此 Codelab 中,我们将构建三个解决方案,这些解决方案将 Gemini Enterprise AI 智能体与 Google Workspace 紧密集成。他们将展示可用于与数据、操作和界面互动的架构模式。

无代码自定义智能体

借助此代理,用户可以使用自然语言搜索数据并针对 Workspace 执行操作。它依赖于以下元素:

  • 模型:Gemini。
  • 数据和操作:Google Workspace(日历、Gmail、云端硬盘、NotebookLM)的 Gemini Enterprise 数据存储区、Google 搜索
  • 代理构建工具:Gemini Enterprise Agent Designer。
  • 代理宿主:Gemini Enterprise。
  • 界面:Gemini Enterprise Web 应用。

90e42539e5959634.png

60e62437ce29a818.png

专业代码自定义代理

借助此代理,用户可以使用自定义工具和规则,以自然语言搜索数据并针对 Workspace 执行操作。它依赖于以下元素:

  • 模型:Gemini。
  • 数据和操作:Google Workspace(日历、Gmail、云端硬盘、NotebookLM)的 Gemini Enterprise 数据存储区、Google 搜索、Google 管理的 Vertex AI 搜索模型上下文协议 (MCP) 服务器、用于发送 Google Chat 消息(通过 Google Chat API)的自定义工具函数。
  • 智能体构建工具:智能体开发套件 (ADK)。
  • 代理主机:Vertex AI Agent Engine。
  • 界面:Gemini Enterprise Web 应用。

1647ebff031c42e7.png

a8087d2351e77fb4.png

作为 Google Workspace 加购项的默认代理

借助此代理,用户可以在 Workspace 应用界面中以自然语言搜索 Workspace 数据。它依赖于以下元素:

  • 模型:Gemini。
  • 数据:Google Workspace(日历、Gmail、云端硬盘、NotebookLM)的 Gemini Enterprise 数据存储区、Google 搜索
  • 代理宿主:Gemini Enterprise。
  • 界面:适用于 Chat 和 Gmail 的 Google Workspace 加购项(可轻松扩展到 Google 日历、云端硬盘、文档、表格和幻灯片)。
  • Google Workspace 加购项:Apps 脚本、Gemini Enterprise 和 Vertex AI API、上下文(用户元数据、所选 Gmail 邮件)。

c8c63fb3f324fecf.png

d33b8cb50ee251b7.png

学习内容

  • Gemini Enterprise 与 Google Workspace 之间可实现数据和操作的集成点。
  • 用于构建托管在 Gemini Enterprise 中的自定义智能体的无代码和专业代码选项。
  • 用户可以通过哪些方式从 Gemini Enterprise Web 应用和 Google Workspace 应用访问代理。

2. 进行设置

查看概念

Gemini Enterprise 应用

Gemini Enterprise 应用可为最终用户提供搜索结果、操作和智能体。在 API 的上下文中,“应用”一词可以与“引擎”一词互换使用。应用必须连接到数据存储区,才能使用其中的数据来提供搜索结果、答案或操作。

Gemini Enterprise Web 应用

Gemini Enterprise Web 应用与 Gemini Enterprise 应用相关联。它是一个集中式 AI 主页,员工可在此处使用单个聊天界面搜索孤立的公司数据、运行专门的 AI 代理来处理复杂的工作流程,并生成具有企业级隐私保护功能的专业级内容。

初始化和访问资源

在本部分中,您可以使用自己偏好的网络浏览器访问和配置以下资源。

Gemini Enterprise 应用

在新标签页中打开 Google Cloud 控制台,然后按以下步骤操作:

  1. 选择您的项目。
  2. 在 Google Cloud 搜索字段中,搜索并选择 Gemini Enterprise,然后点击 + 创建应用。如果您没有 Gemini Enterprise 许可,系统会提示您激活 30 天免费试用许可。

  1. 应用名称设置为 codelab
  2. 系统会根据名称生成 ID,并显示在相应字段下方,请复制该 ID。
  3. 多区域设置为 global (Global)
  4. 点击创建

8712ada39377205e.png

  1. 应用创建完成后,系统会自动将您重定向到 Gemini Enterprise > 概览
  2. 获取完整访问权限下,点击设置身份
  3. 在新界面中,选择使用 Google Identity,然后点击确认员工身份

3209c156eff4ba43.png

  1. 系统会保存配置,并自动将您重定向到 Gemini Enterprise > 概览
  2. 前往配置
  3. 功能管理标签页中,开启启用代理设计工具,然后点击保存

f0cd9da419b41cb6.png

Gemini Enterprise Web 应用

在 Cloud 控制台中打开 Gemini Enterprise,然后按以下步骤操作:

  1. 点击名为 codelab 的应用。
  2. 复制显示的网址,因为我们将在后续步骤中使用该网址来前往 Gemini Enterprise Web 应用。

b46ee6176744565d.png

3. 无代码自定义代理

借助此代理,用户可以使用自然语言搜索数据并针对 Workspace 执行操作。它依赖于以下元素:

  • 模型:Gemini。
  • 数据和操作:Google Workspace(日历、Gmail、云端硬盘、NotebookLM)的 Gemini Enterprise 数据存储区、Google 搜索
  • 代理构建工具:Gemini Enterprise Agent Designer。
  • 代理宿主:Gemini Enterprise。
  • 界面:Gemini Enterprise Web 应用。

查看概念

Gemini

Gemini 是 Google 推出的一款多模态 LLM。它能帮助人们释放潜力,从而加强想象力、增加好奇心并提高工作效率。

Gemini Enterprise 数据存储区

Gemini Enterprise 数据存储区是一种实体,其中包含从第一方数据源(例如 Google Workspace)或第三方应用(例如 Jira 或 Salesforce)提取的数据。包含第三方应用数据的数据存储区也称为数据连接器。

Gemini Enterprise Agent Designer

Gemini Enterprise 智能体设计工具是一个交互式无代码/低代码平台,用于在 Gemini Enterprise 中创建、管理和启动单步和多步智能体。

查看解决方案架构

e77aafb772502aaf.png

启用 API

Gemini Enterprise Workspace 数据存储区需要启用以下 API:

  1. Google Cloud 控制台中,启用 Calendar API、Gmail API 和 People API:

573322606b715a69.png

  1. 依次点击菜单 ☰ > API 和服务 > 已启用的 API 和服务,然后确认列表中包含 Google Calendar APIGmail APIPeople API

Gemini Enterprise Workspace 日历和 Gmail 操作需要配置权限请求页面:

  1. Google Cloud 控制台中,依次点击菜单 ☰ > Google Auth Platform > 品牌推广

  1. 点击开始使用
  2. 应用信息下,将应用名称设置为 Codelab
  3. 用户支持电子邮件中,选择一个支持电子邮件地址,以便用户在对自己的同意情况有疑问时与您联系。
  4. 点击下一步
  5. 受众下,选择内部
  6. 点击下一步
  7. 联系信息下,输入一个电子邮件地址,以便您接收有关项目变更的通知。
  8. 点击下一步
  9. 完成部分,查看 Google API 服务用户数据政策,如果您同意该政策,请选择我同意 Google API 服务:用户数据政策
  10. 依次点击继续创建

578c2b38219b2f7b.png

  1. 系统会保存配置,并自动将您重定向到 Google Auth Platform > 概览
  2. 前往数据访问权限
  3. 点击添加或移除范围
  4. 复制以下授权范围,然后将其粘贴到手动添加授权范围字段中。
https://www.googleapis.com/auth/calendar.readonly
https://www.googleapis.com/auth/calendar.events
https://www.googleapis.com/auth/calendar.calendars
https://www.googleapis.com/auth/gmail.send
https://www.googleapis.com/auth/gmail.readonly
  1. 依次点击添加到表格更新保存

874b1dda14e8f379.png

如需了解详情,请参阅完整的配置 OAuth 权限请求指南。

创建 OAuth 客户端凭据

为 Gemini Enterprise 创建新的 OAuth 客户端以对用户进行身份验证:

  1. Google Cloud 控制台中,依次点击菜单 ☰ > Google Auth 平台 > 客户端

  1. 点击 + 创建客户端
  2. 应用类型部分,选择 Web 应用
  3. 名称设置为 codelab
  4. 跳过已获授权的 JavaScript 来源
  5. 已获授权的重定向 URI 部分中,点击添加 URI,然后输入 https://vertexaisearch.cloud.google.com/oauth-redirect
  6. 点击创建
  7. 系统会显示一个对话框,其中包含您新创建的 OAuth 客户端 ID 和密钥。请妥善保存此信息。

a46e5ebfb851aea5.png

创建数据存储区

在 Cloud 控制台中打开 Gemini Enterprise,然后按以下步骤操作:

  1. 点击名为 codelab 的应用。
  2. 在导航菜单中,点击关联的数据存储区
  3. 点击 + 新建数据存储区
  4. 来源中,搜索 Google 日历,然后点击选择
  5. 操作部分,输入之前步骤中保存的客户端 ID客户端密钥,然后点击确认身份验证,并按照相应步骤对 OAuth 客户端进行身份验证和授权。
  6. 启用“创建日历活动”和“更新日历活动”操作。
  7. 点击继续

a1d76e70edec0cf.png

  1. 配置部分中,将数据连接器名称设置为 calendar
  2. 点击创建
  3. 系统会自动将您重定向到已关联的数据存储区,您可以在其中看到新添加的数据存储区。

创建 Google Gmail 数据存储区:

  1. 点击 + 新建数据存储区
  2. 来源中,搜索 Google Gmail,然后点击选择
  3. 操作部分,输入之前步骤中保存的客户端 ID客户端密钥,然后点击验证身份验证
  4. 启用发送电子邮件操作。
  5. 点击继续
  6. 配置部分中,将数据连接器名称设置为 gmail
  7. 点击创建
  8. 系统会自动将您重定向到已关联的数据存储区,您可以在其中看到新添加的数据存储区。

创建 Google 云端硬盘数据存储区:

  1. 点击 + 新建数据存储区
  2. 来源中,搜索 Google 云端硬盘,然后点击选择
  3. 数据部分,选择全部,然后点击继续
  4. 配置部分中,将数据连接器名称设置为 drive
  5. 点击创建
  6. 系统会自动将您重定向到已关联的数据存储区,您可以在其中看到新添加的数据存储区。

创建 NotebookLM 数据存储区:

  1. 点击 + 新建数据存储区
  2. 来源中,搜索 NotebookLM,然后点击选择
  3. 配置部分中,将数据连接器名称设置为 notebooklm
  4. 点击创建
  5. 系统会自动将您重定向到已关联的数据存储区,您可以在其中看到新添加的数据存储区。

几分钟后,所有已连接的数据存储区(NotebookLM 除外)的状态都将变为有效。如果您看到任何错误,可以点击相应数据源来查看错误详情。

ceba9eb2480a2696.png

测试数据存储区

打开我们之前复制的 Gemini Enterprise Web 应用网址:

  1. 依次点击菜单 ☰ > 新对话
  2. 在新聊天消息字段的页脚中,点击连接器图标,然后启用所有连接器。
  3. 您现在可以尝试使用与连接器相关的提示。例如,在对话中,输入 Do I have any meetings today? 并按 enter
  4. 接下来,尝试输入 How many emails did I receive today? 并按 enter
  5. 最后,输入 Give me the title of the last Drive file I created,然后按 enter

90e42539e5959634.png

创建自定义代理

在 Gemini Enterprise Web 应用中,使用代理设计工具创建新代理:

  1. 依次点击“菜单”图标 ☰ >“+ 新建代理”
  2. 在聊天中,输入 An agent that always sends pirate-themed emails but use normal English otherwise,然后按 enter

2803c1dedd20433e.png

  1. Agent Designer 会根据提示起草代理,并在编辑器中打开该代理。
  2. 点击创建

试用自定义代理

  1. 在 Gemini Enterprise Web 应用中,与新创建的代理对话:
  2. 依次点击“菜单”图标 ☰ >“代理”
  3. 您的代理下选择相应代理。
  4. 在新对话消息字段的页脚中,点击连接器图标,然后点击启用操作(针对邮件),并按照说明授权代理
  5. 在聊天中,输入 Send an email to someone@example.com saying I'll see them at Cloud Next, generate some subject and body yourself,然后按 enter。您可以将示例电子邮件地址替换为您的电子邮件地址。
  6. 点击“✔️”即可发送电子邮件。

60e62437ce29a818.png

d4fb65d14fdf27da.png

4. 专业代码自定义代理

借助此代理,用户可以使用自定义工具和规则,以自然语言搜索数据并针对 Workspace 执行操作。它依赖于以下元素:

  • 模型:Gemini。
  • 数据和操作:Google Workspace(日历、Gmail、云端硬盘、NotebookLM)的 Gemini Enterprise 数据存储区、Google 搜索、Google 管理的 Vertex AI 搜索模型上下文协议 (MCP) 服务器、用于发送 Google Chat 消息(通过 Google Chat API)的自定义工具函数。
  • 智能体构建工具:智能体开发套件 (ADK)。
  • 代理主机:Vertex AI Agent Engine。
  • 界面:Gemini Enterprise Web 应用。

它将使用 自带功能集成到 Gemini Enterprise 中,因此我们需要完成部署、注册和配置步骤。

查看概念

Vertex AI

Vertex AI 提供构建和使用生成式 AI 所需的一切内容,包括 AI 解决方案、搜索和对话、130 多个基础模型以及统一的 AI 平台。

4670fcf7a826af4d.png

智能体开发套件 (ADK)

智能体开发套件 (ADK) 是一套专门的工具和框架,旨在通过提供用于推理、内存管理和工具集成的预构建模块,简化自主 AI 智能体的创建过程。

Model Context Protocol (MCP)

Model Context Protocol (MCP) 是一种开放标准,旨在通过通用的“即插即用”接口,在 AI 应用与各种数据源或工具之间实现无缝、安全的集成。

函数工具

函数工具是一种预定义的执行例程,AI 模型可以触发该例程来执行特定操作或从外部系统检索实时数据,从而将其功能扩展到简单的文本生成之外。

查看解决方案架构

43df337e0f3d64e8.png

查看源代码

agent.py

...
MODEL = "gemini-2.5-flash"

# Gemini Enterprise authentication injects a bearer token into the ToolContext state.
# The key pattern is "GE_AUTH_NAME_<random_digits>".
# We dynamically parse this token to authenticate our MCP and API calls.
GE_AUTH_NAME = "enterprise-ai"

VERTEXAI_SEARCH_TIMEOUT = 15.0

def get_project_id():
    """Fetches the consumer project ID from the environment natively."""
    _, project = google.auth.default()
    if project:
        return project
    raise Exception(f"Failed to resolve GCP Project ID from environment.")

def find_serving_config_path():
    """Dynamically finds the default serving config in the engine."""
    project_id = get_project_id()
    engines = discoveryengine_v1.EngineServiceClient().list_engines(
        parent=f"projects/{project_id}/locations/global/collections/default_collection"
    )
    for engine in engines:
        # engine.name natively contains the numeric Project Number
        return f"{engine.name}/servingConfigs/default_serving_config"
    raise Exception(f"No Discovery Engines found in project {project_id}")

def _get_access_token_from_context(tool_context: ToolContext) -> str:
    """Helper method to dynamically parse the intercepted bearer token from the context state."""
    escaped_name = re.escape(GE_AUTH_NAME)
    pattern = re.compile(fr"^{escaped_name}_\d+$")
    # Handle ADK varying state object types (Raw Dict vs ADK State)
    state_dict = tool_context.state.to_dict() if hasattr(tool_context.state, 'to_dict') else tool_context.state
    matching_keys = [k for k in state_dict.keys() if pattern.match(k)]
    if matching_keys:
        return state_dict.get(matching_keys[0])
    raise Exception(f"No bearer token found in ToolContext state matching pattern {pattern.pattern}")

def auth_header_provider(tool_context: ToolContext) -> dict[str, str]:
    token = _get_access_token_from_context(tool_context)
    return {"Authorization": f"Bearer {token}"}

def send_direct_message(email: str, message: str, tool_context: ToolContext) -> dict:
    """Sends a Google Chat Direct Message (DM) to a specific user by email address."""
    chat_client = chat_v1.ChatServiceClient(
        credentials=Credentials(token=_get_access_token_from_context(tool_context))
    )

    # 1. Setup the DM space or find existing one
    person = chat_v1.User(
        name=f"users/{email}",
        type_=chat_v1.User.Type.HUMAN
    )
    membership = chat_v1.Membership(member=person)
    space_req = chat_v1.Space(space_type=chat_v1.Space.SpaceType.DIRECT_MESSAGE)
    setup_request = chat_v1.SetUpSpaceRequest(
        space=space_req,
        memberships=[membership]
    )
    space_response = chat_client.set_up_space(request=setup_request)
    space_name = space_response.name
    
    # 2. Send the message
    msg = chat_v1.Message(text=message)
    message_request = chat_v1.CreateMessageRequest(
        parent=space_name,
        message=msg
    )
    message_response = chat_client.create_message(request=message_request)
    
    return {"status": "success", "message_id": message_response.name, "space": space_name}

vertexai_mcp = McpToolset(
    connection_params=StreamableHTTPConnectionParams(
        url="https://discoveryengine.googleapis.com/mcp",
        timeout=VERTEXAI_SEARCH_TIMEOUT,
        sse_read_timeout=VERTEXAI_SEARCH_TIMEOUT
    ),
    tool_filter=['search'],
    # The auth_header_provider dynamically injects the bearer token from the ToolContext
    # into the MCP call for authentication.
    header_provider=auth_header_provider
)

# Answer nicely the following user queries:
#  - Please find my meetings for today, I need their titles and links
#  - What is the latest Drive file I created?
#  - What is the latest Gmail message I received?
#  - Please send the following message to someone@example.com: Hello, this is a test message.

root_agent = LlmAgent(
    model=MODEL,
    name='enterprise_ai',
    instruction=f"""
        You are a helpful assistant that always uses the Vertex AI MCP search tool to answer the user's message, unless the user asks you to send a message to someone.
        If the user asks you to send a message to someone, use the send_direct_message tool to send the message.
        You MUST unconditionally use the Vertex AI MCP search tool to find answer, even if you believe you already know the answer or believe the Vertex AI MCP search tool does not contain the data.
        The Vertex AI MCP search tool accesses the user's data through datastores including Google Drive, Google Calendar, and Gmail.
        Only use the Vertex AI MCP search tool with servingConfig and query parameters, do not use any other parameters.
        Always use the servingConfig {find_serving_config_path()} while using the Vertex AI MCP search tool.
    """,
    tools=[vertexai_mcp, FunctionTool(send_direct_message)]
)

启用 API

此解决方案需要启用其他 API:

  1. Google Cloud 控制台中,启用 Vertex AI、Cloud Resource Manager 和 Google Chat API:

4f02a36b050bab00.png

  1. 依次点击菜单 ☰ > API 和服务 > 已启用的 API 和服务,然后确认列表中包含 Vertex AI APICloud Resource Manager APIGoogle Chat API

此解决方案需要额外的数据访问权限:

  1. Google Cloud 控制台中,依次点击菜单 ☰ > Google Auth Platform > 数据访问权限

  1. 点击添加或移除范围
  2. 复制以下授权范围,然后将其粘贴到手动添加授权范围字段中。
  3. 依次点击添加到表格更新保存
https://www.googleapis.com/auth/cloud-platform
https://www.googleapis.com/auth/chat.messages.create
https://www.googleapis.com/auth/chat.spaces.create
  1. 依次点击添加到表格更新保存

56fbba733139acfe.png

更新 OAuth 客户端凭据

此解决方案需要额外的已获授权的重定向 URI:

  1. Google Cloud 控制台中,依次点击菜单 ☰ > Google Auth 平台 > 客户端

  1. 点击客户名称 codelab
  2. 已获授权的重定向 URI 部分中,点击添加 URI,然后输入 https://vertexaisearch.cloud.google.com/static/oauth/oauth.html
  3. 点击保存

deed597aa54fec91.png

启用 Vertex AI Search MCP

  1. 在终端中,执行以下命令:
gcloud beta services mcp enable discoveryengine.googleapis.com \
     --project=$(gcloud config get-value project)

配置 Chat 应用

  1. Google Cloud 控制台中,在 Google Cloud 搜索字段中搜索 Google Chat API,点击 Google Chat API,然后依次点击管理配置

  1. 应用名称说明设置为 Gemini Enterprise
  2. 头像网址设置为 https://developers.google.com/workspace/add-ons/images/quickstart-app-avatar.png
  3. 取消选中启用互动功能,然后在随即显示的模态对话框中点击停用
  4. 选择将错误记录到 Logging
  5. 点击保存

90cb612e51bce4e6.png

在 Vertex AI Agent Engine 中部署智能体

  1. 下载此 GitHub 代码库

  1. 在终端中,打开 solutions/enterprise-ai-agent 目录,然后执行以下命令:
# 1. Create and activate a new virtual environment
python3 -m venv .venv
source .venv/bin/activate

# 2. Install poetry and project dependencies
pip install poetry
poetry install

# 3. Deploy the agent
adk deploy agent_engine \
  --project=$(gcloud config get-value project) \
  --region=us-central1 \
  --display_name="Enterprise AI" \
  enterprise_ai

eafd2f9c4fbf305.png

  1. 当您在日志中看到“正在部署到代理引擎...”这一行时,请打开新终端并执行以下命令,以向 Vertex AI Reasoning Engine 服务代理添加所需权限:
# 1. Get the current Project ID
PROJECT_ID=$(gcloud config get-value project)

# 2. Extract the Project Number for that ID
PROJECT_NUMBER=$(gcloud projects describe $PROJECT_ID --format='value(projectNumber)')

# 3. Construct the Service Account name
SERVICE_ACCOUNT="service-${PROJECT_NUMBER}@gcp-sa-aiplatform-re.iam.gserviceaccount.com"

# 4. Apply the IAM policy binding
gcloud projects add-iam-policy-binding $PROJECT_ID \
     --member="serviceAccount:$SERVICE_ACCOUNT" \
     --role="roles/discoveryengine.viewer"
  1. 等待 adk deploy 命令完成,然后从命令输出中以绿色显示的已部署的新智能体的资源名称。

d098fe1347d6581b.png

在 Gemini Enterprise 中注册代理

在 Cloud 控制台中打开 Gemini Enterprise,然后按以下步骤操作:

  1. 点击名为 codelab 的应用。
  2. 在导航菜单中,点击代理
  3. 点击 + 添加代理
  4. 对于通过 Agent Engine 构建的自定义代理,点击添加。系统会显示授权部分。
  5. 点击添加授权
  6. 授权名称设置为 enterprise-ai。系统会根据名称生成 ID,并显示在相应字段下方,请复制该 ID。
  7. 客户端 ID 设置为与之前步骤中创建和更新的 OAuth 客户端相同的值。
  8. 客户端密钥设置为与之前步骤中创建和更新的 OAuth 客户端相同的值。
  9. 令牌 URI 设置为 https://oauth2.googleapis.com/token
  10. 授权 URI 设置为以下值,并将 <CLIENT_ID> 替换为在上一步中创建和更新的 OAuth 客户端 ID。
https://accounts.google.com/o/oauth2/v2/auth?client_id=<CLIENT_ID>&redirect_uri=https%3A%2F%2Fvertexaisearch.cloud.google.com%2Fstatic%2Foauth%2Foauth.html&scope=https%3A%2F%2Fwww.googleapis.com%2Fauth%2Fcalendar.readonly%20https%3A%2F%2Fwww.googleapis.com%2Fauth%2Fcalendar.calendars%20https%3A%2F%2Fwww.googleapis.com%2Fauth%2Fcalendar.events%20https%3A%2F%2Fwww.googleapis.com%2Fauth%2Fcloud-platform%20https%3A%2F%2Fwww.googleapis.com%2Fauth%2Fgmail.send%20https%3A%2F%2Fwww.googleapis.com%2Fauth%2Fgmail.readonly%20https%3A%2F%2Fwww.googleapis.com%2Fauth%2Fchat.messages.create%20https%3A%2F%2Fwww.googleapis.com%2Fauth%2Fchat.spaces.create&include_granted_scopes=true&response_type=code&access_type=offline&prompt=consent
  1. 点击完成,然后点击下一步。系统会显示配置部分。
  2. 代理名称代理说明设置为 Enterprise AI
  3. 代理引擎推理引擎设置为在之前的步骤中复制的推理引擎资源名称。其格式如下:
projects/<PROJECT_ID>/locations/<LOCATION>/reasoningEngines/<REASONING_ENGINE_ID>
  1. 点击创建。新添加的代理现已列在代理下。

试用代理

  1. 在 Gemini Enterprise Web 应用中,与新注册的代理对话:
  2. 依次点击“菜单”图标 ☰ >“代理”
  3. 选择来自您的组织下方的代理。
  4. 在聊天中,输入 Please find my meetings for today, I need their titles and links,然后按 enter
  5. 点击授权,然后按照授权流程操作。

ed61cf654cbcd76c.png

  1. 智能体回答时会列出日历活动(具体取决于用户的账号)。
  2. 在聊天中,输入 Please send a Chat message to someone@example.com with the following text: Hello!,然后按 enter
  3. 代理会以确认消息做出回答。

1647ebff031c42e7.png

a8087d2351e77fb4.png

5. 作为 Google Workspace 插件的默认代理

借助此代理,用户可以在 Workspace 应用界面中以自然语言搜索 Workspace 数据。它依赖于以下元素:

  • 模型:Gemini。
  • 数据:Google Workspace(日历、Gmail、云端硬盘、NotebookLM)的 Gemini Enterprise 数据存储区、Google 搜索
  • 代理宿主:Gemini Enterprise。
  • 界面:适用于 Chat 和 Gmail 的 Google Workspace 加购项(可轻松扩展到 Google 日历、云端硬盘、文档、表格和幻灯片)。
  • Google Workspace 加购项:Apps 脚本、Gemini Enterprise 和 Vertex AI API、上下文(用户元数据、所选 Gmail 邮件)。

Google Workspace 加载项将使用 StreamAssist API 连接到 Gemini Enterprise。

查看概念

Google Workspace 加购项

Google Workspace 加购项是一种自定义应用,可扩展一个或多个 Google Workspace 应用(Gmail、Chat、Google 日历、Google 文档、云端硬盘、Meet、Google 表格和 Google 幻灯片)。

Apps 脚本

Apps 脚本是一个基于云的 JavaScript 平台,由 Google 云端硬盘提供支持,可让您与 Google 各项产品集成并自动执行任务。

Google Workspace 卡片框架

Google Workspace 中的 Card 框架可让开发者创建丰富多样的互动式界面。借助它,您可以构建包含文本、图片、按钮和其他 widget 的有条理且美观的卡片。这些卡片可提供结构化信息,并直接在 Workspace 应用中启用快捷操作,从而提升用户体验。

查看解决方案架构

1798c39f7aaed8fc.png

查看源代码

appsscript.json

...
"addOns": {
    "common": {
      "name": "Enterprise AI",
      "logoUrl": "https://developers.google.com/workspace/add-ons/images/quickstart-app-avatar.png"
    },
    "chat": {},
    "gmail": {
      "contextualTriggers": [
        {
          "unconditional": {},
          "onTriggerFunction": "onAddonEvent"
        }
      ]
    }
  },
  "oauthScopes": [
    "https://www.googleapis.com/auth/script.external_request",
    "https://www.googleapis.com/auth/discoveryengine.assist.readwrite",
    "https://www.googleapis.com/auth/gmail.addons.execute",
    "https://www.googleapis.com/auth/gmail.addons.current.message.readonly"
  ]
...

Chat.gs

...
// Service that handles Google Chat operations.

// Handle incoming Google Chat message events, actions will be taken via Google Chat API calls
function onMessage(event) {
  if (isInDebugMode()) {
    console.log(`MESSAGE event received (Chat): ${JSON.stringify(event)}`);
  }
  // Extract data from the event.
  const chatEvent = event.chat;
  setChatConfig(chatEvent.messagePayload.space.name);

  // Request AI agent to answer the message
  requestAgent(chatEvent.messagePayload.message);
  // Respond with an empty response to the Google Chat platform to acknowledge execution
  return null; 
}

// --- Utility functions ---

// The Chat direct message (DM) space associated with the user
const SPACE_NAME_PROPERTY = "DM_SPACE_NAME"

// Sets the Chat DM space name for subsequent operations.
function setChatConfig(spaceName) {
  const userProperties = PropertiesService.getUserProperties();
  userProperties.setProperty(SPACE_NAME_PROPERTY, spaceName);
  console.log(`Space is set to ${spaceName}`);
}

// Retrieved the Chat DM space name to sent messages to.
function getConfiguredChat() {
  const userProperties = PropertiesService.getUserProperties();
  return userProperties.getProperty(SPACE_NAME_PROPERTY);
}

// Finds the Chat DM space name between the Chat app and the given user.
function findChatAppDm(userName) {
  return Chat.Spaces.findDirectMessage(
    { 'name': userName },
    {'Authorization': `Bearer ${getAddonCredentials().getAccessToken()}`}
  ).name;
}

// Creates a Chat message in the configured space.
function createMessage(message) {
  const spaceName = getConfiguredChat();
  console.log(`Creating message in space ${spaceName}...`);
  return Chat.Spaces.Messages.create(
    message,
    spaceName,
    {},
    {'Authorization': `Bearer ${getAddonCredentials().getAccessToken()}`}
  ).name;
}

Sidebar.gs

...
// Service that handles Gmail operations.

// Triggered when the user opens the Gmail Add-on or selects an email.
function onAddonEvent(event) {
  // If this was triggered by a button click, handle it
  if (event.parameters && event.parameters.action === 'send') {
    return handleSendMessage(event);
  }

  // Otherwise, just render the default initial sidebar
  return createSidebarCard();
}

// Creates the standard Gmail sidebar card consisting of a text input and send button.
// Optionally includes an answer section if a response was generated.
function createSidebarCard(optionalAnswerSection) {
  const card = CardService.newCardBuilder();
  const actionSection = CardService.newCardSection();

  // Create text input for the user's message
  const messageInput = CardService.newTextInput()
    .setFieldName("message")
    .setTitle("Message")
    .setMultiline(true);

  // Create action for sending the message
  const sendAction = CardService.newAction()
    .setFunctionName('onAddonEvent')
    .setParameters({ 'action': 'send' });

  const sendButton = CardService.newTextButton()
    .setText("Send message")
    .setTextButtonStyle(CardService.TextButtonStyle.FILLED)
    .setOnClickAction(sendAction);

  actionSection.addWidget(messageInput);
  actionSection.addWidget(CardService.newButtonSet().addButton(sendButton));

  card.addSection(actionSection);

  // Attach the response at the bottom if we have one
  if (optionalAnswerSection) {
    card.addSection(optionalAnswerSection);
  }

  return card.build();
}

// Handles clicks from the Send message button.
function handleSendMessage(event) {
  const commonEventObject = event.commonEventObject || {};
  const formInputs = commonEventObject.formInputs || {};
  const messageInput = formInputs.message;

  let userMessage = "";
  if (messageInput && messageInput.stringInputs && messageInput.stringInputs.value.length > 0) {
    userMessage = messageInput.stringInputs.value[0];
  }

  if (!userMessage || userMessage.trim().length === 0) {
    return CardService.newActionResponseBuilder()
      .setNotification(CardService.newNotification().setText("Please enter a message."))
      .build();
  }

  let finalQueryText = `USER MESSAGE TO ANSWER: ${userMessage}`;

  // If we have an email selected in Gmail, append its content as context
  if (event.gmail && event.gmail.messageId) {
    try {
      GmailApp.setCurrentMessageAccessToken(event.gmail.accessToken);
      const message = GmailApp.getMessageById(event.gmail.messageId);

      const subject = message.getSubject();
      const bodyText = message.getPlainBody() || message.getBody();

      finalQueryText += `\n\nEMAIL THE USER HAS OPENED ON SCREEN:\nSubject: ${subject}\nBody:\n---\n${bodyText}\n---`;
    } catch (e) {
      console.error("Could not fetch Gmail context: " + e);
      // Invalidate the token explicitly so the next prompt requests the missing scopes
      ScriptApp.invalidateAuth();

      CardService.newAuthorizationException()
        .setResourceDisplayName("Enterprise AI")
        .setAuthorizationUrl(ScriptApp.getAuthorizationUrl())
        .throwException();
    }
  }

  try {
    const responseText = queryAgent({ text: finalQueryText, forceNewSession: true });

    // We leverage the 'showdown' library to parse the LLM's Markdown output into HTML
    // We also substitute markdown listings with arrows and adjust newlines for clearer rendering in the sidebar
    let displayedText = substituteListingsFromMarkdown(responseText);
    displayedText = new showdown.Converter().makeHtml(displayedText).replace(/\n/g, '\n\n');

    const textParagraph = CardService.newTextParagraph();
    textParagraph.setText(displayedText);

    const answerSection = CardService.newCardSection()
      .addWidget(textParagraph);

    const updatedCard = createSidebarCard(answerSection);

    return CardService.newActionResponseBuilder()
      .setNavigation(CardService.newNavigation().updateCard(updatedCard))
      .build();

  } catch (err) {
    return CardService.newActionResponseBuilder()
      .setNotification(CardService.newNotification().setText("Error fetching response: " + err.message))
      .build();
  }
}
...

AgentHandler.gs

...
// Service that handles Gemini Enterprise AI Agent operations.

// Submits a query to the AI agent and returns the response string synchronously
function queryAgent(input) {
  const isNewSession = input.forceNewSession || !PropertiesService.getUserProperties().getProperty(AGENT_SESSION_NAME);
  const sessionName = input.forceNewSession ? createAgentSession() : getOrCreateAgentSession();

  let systemPrompt = "SYSTEM PROMPT START Do not respond with tables but use bullet points instead.";
  if (input.forceNewSession) {
    systemPrompt += " Do not ask the user follow-up questions or converse with them as history is not kept in this interface.";
  }
  systemPrompt += " SYSTEM PROMPT END\n\n";

  const queryText = isNewSession ? systemPrompt + input.text : input.text;

  const requestPayload = {
    "session": sessionName,
    "userMetadata": { "timeZone": Session.getScriptTimeZone() },
    "query": { "text": queryText },
    "toolsSpec": { "vertexAiSearchSpec": { "dataStoreSpecs": getAgentDataStores().map(ds => { dataStore: ds }) } },
    "agentsSpec": { "agentSpecs": [{ "agentId": getAgentId() }] }
  };

  const responseContentText = UrlFetchApp.fetch(
    `https://${getLocation()}-discoveryengine.googleapis.com/v1alpha/${getReasoningEngine()}/assistants/default_assistant:streamAssist?alt=sse`,
    {
      method: 'post',
      headers: { 'Authorization': `Bearer ${ScriptApp.getOAuthToken()}` },
      contentType: 'application/json',
      payload: JSON.stringify(requestPayload),
      muteHttpExceptions: true
    }
  ).getContentText();

  if (isInDebugMode()) {
    console.log(`Response: ${responseContentText}`);
  }

  const events = responseContentText.split('\n').map(s => s.replace(/^data:\s*/, '')).filter(s => s.trim().length > 0);
  console.log(`Received ${events.length} agent events.`);

  let answerText = "";
  for (const eventJson of events) {
    if (isInDebugMode()) {
      console.log("Event: " + eventJson);
    }
    const event = JSON.parse(eventJson);

    // Ignore internal events
    if (!event.answer) {
      console.log(`Ignored: internal event`);
      continue;
    }

    // Handle text replies
    const replies = event.answer.replies || [];
    for (const reply of replies) {
      const content = reply.groundedContent.content;
      if (content) {
        if (isInDebugMode()) {
          console.log(`Processing content: ${JSON.stringify(content)}`);
        }
        if (content.thought) {
          console.log(`Ignored: thought event`);
          continue;
        }
        answerText += content.text;
      }
    }

    if (event.answer.state === "SUCCEEDED") {
      console.log(`Answer text: ${answerText}`);
      return answerText;
    } else if (event.answer.state !== "IN_PROGRESS") {
      throw new Error("Something went wrong, check the Apps Script logs for more info.");
    }
  }
  return answerText;
}

// Gets the list of data stores configured for the agent to include in the request.
function getAgentDataStores() {
  const responseContentText = UrlFetchApp.fetch(
    `https://${getLocation()}-discoveryengine.googleapis.com/v1/${getReasoningEngine().split('/').slice(0, 6).join('/')}/dataStores`,
    {
      method: 'get',
      // Use the add on service account credentials for data store listing access
      headers: { 'Authorization': `Bearer ${getAddonCredentials().getAccessToken()}` },
      contentType: 'application/json',
      muteHttpExceptions: true
    }
  ).getContentText();
  if (isInDebugMode()) {
    console.log(`Response: ${responseContentText}`);
  }
  const dataStores = JSON.parse(responseContentText).dataStores.map(ds => ds.name);
  if (isInDebugMode()) {
    console.log(`Data stores: ${dataStores}`);
  }
  return dataStores;
}
...

启动服务账号

Google Cloud 控制台中,按以下步骤操作:

  1. 依次点击菜单 ☰ > IAM 和管理 > 服务账号 > + 创建服务账号

  1. 服务账号名称设置为 ge-add-on

d44d6aae29e2464c.png

  1. 点击创建并继续
  2. 在权限中添加 Discovery Engine 查看者角色。

f1374efa4f326ef5.png

  1. 依次点击继续完成。您将会转到服务账号页面,并看到创建的服务账号。

b9496085f1404c5c.png

  1. 选择新创建的服务账号,然后选择密钥标签页。
  2. 依次点击添加密钥创建新密钥
  3. 选择 JSON,然后点击创建

f4280f5533a08821.png

  1. 对话框随即关闭,新创建的公钥/私钥对将以 JSON 文件的形式自动下载到本地环境。

创建和配置 Apps 脚本项目

  1. 点击以下按钮,打开 Enterprise AI 加购项 Apps 脚本项目:

  1. 依次点击概览 > 复制
  2. 在您的 Apps 脚本项目中,依次点击项目设置 > 修改脚本属性 > 添加脚本属性,以添加脚本属性。
  3. REASONING_ENGINE_RESOURCE_NAME 设置为 Gemini Enterprise 应用资源名称。其格式如下:
# 1. Replace PROJECT_ID with the Google Cloud project ID.
# 2. Replace GE_APP_ID with the codelab app ID found in Google Cloud console > Gemini Enterprise > Apps.

projects/<PROJECT_ID>/locations/global/collections/default_collection/engines/<GE_APP_ID>
  1. APP_SERVICE_ACCOUNT_KEY 设置为在之前的步骤中下载的服务账号文件中的 JSON 密钥。
  2. 点击保存脚本属性

部署到 Gmail 和 Chat

在您的 Apps 脚本项目中,按以下步骤操作:

  1. 依次点击部署 > 测试部署,然后点击安装。现在,您可以在 Gmail 中使用此功能。
  2. 点击主要部署 ID 下方的复制

2ed2df972ad92715.png

Google Cloud 控制台中,按以下步骤操作:

  1. 在 Google Cloud 搜索字段中搜索 Google Chat API,点击 Google Chat API,然后依次点击管理配置

  1. 选择启用互动功能
  2. 取消选择加入聊天室和群组对话
  3. 连接设置下,选择 Apps 脚本
  4. 部署 ID 设置为在上一步中复制的头部部署 ID
  5. 公开范围下,选择面向您 Workspace 网域中的特定人员和群组提供此 Chat 扩展应用,然后输入您的电子邮件地址。
  6. 点击保存

3b7d461c423f7c51.png

试用加购项

在新标签页中打开 Google Chat,然后按以下步骤操作:

  1. 打开与 Chat 应用 Gemini Enterprise 的私信对话。

3da8690d19baf2d0.png

  1. 点击配置,然后完成身份验证流程。
  2. 输入 What are my meetings for today?,然后按 enterGemini Enterprise 聊天应用应回复结果。

c8c63fb3f324fecf.png

在新标签页中打开 Gmail,然后按以下步骤操作:

  1. 给自己发送一封电子邮件,其中主题设置为 We need to talk正文设置为 Are you available today between 8 and 9 AM?
  2. 打开新收到的电子邮件。
  3. 打开企业版 AI 加购项边栏。
  4. 消息设置为 Am I?
  5. 点击发送讯息
  6. 答案会显示在按钮下方。

d33b8cb50ee251b7.png

6. 清理

删除 Google Cloud 项目

为避免系统因此 Codelab 中使用的资源向您的 Google Cloud 账号收取费用,我们建议您删除该 Google Cloud 项目。

Google Cloud 控制台中,按以下步骤操作:

  1. 依次点击菜单 ☰ > IAM 和管理 > 设置

  1. 点击关停
  2. 输入项目 ID。
  3. 点击仍要关停

3b9492d97f771b2c.png

7. 恭喜

恭喜!您构建的解决方案可让 Gemini Enterprise 和 Google Workspace 更紧密地结合在一起,从而为员工提供强大助力!

后续操作

在此 Codelab 中,我们仅展示了最典型的用例,但您或许需要在解决方案中考虑很多值得扩展的地方,例如:

  • 使用 Gemini CLI 和 Antigravity 等 AI 赋能的开发者工具。
  • 与其他代理框架和工具集成,例如自定义 MCP、自定义函数调用和生成式界面。
  • 与在 Vertex AI 等专用平台上托管的其他 AI 模型(包括自定义模型)集成。
  • 与托管在专用平台(例如 Dialogflow)中或由第三方通过 Cloud Marketplace 托管的其他代理集成。
  • 在 Cloud Marketplace 上发布代理,赋能团队、组织或公共用户。

了解详情

我们为开发者提供了大量资源,例如 YouTube 视频、文档网站、代码示例和教程: