Skip to content
Merged
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
63 changes: 63 additions & 0 deletions .github/workflows/test_connectors_demo.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
# Copyright(C) 2025-2026 Advanced Micro Devices, Inc. All rights reserved.
# SPDX-License-Identifier: MIT

# Tests the GAIA Connectors Demo agent, which ships as the standalone
# gaia-agent-connectors-demo wheel (#1102).

name: Connectors Demo Agent Tests

on:
workflow_call:
push:
branches: [ main ]
paths:
- 'hub/agents/python/connectors-demo/**'
- 'src/gaia/agents/base/**'
- 'src/gaia/connectors/**'
- 'setup.py'
- '.github/workflows/test_connectors_demo.yml'
pull_request:
branches: [ main ]
types: [opened, synchronize, reopened, ready_for_review]
paths:
- 'hub/agents/python/connectors-demo/**'
- 'src/gaia/agents/base/**'
- 'src/gaia/connectors/**'
- 'setup.py'
- '.github/workflows/test_connectors_demo.yml'
merge_group:
workflow_dispatch:

concurrency:
group: ${{ github.workflow }}-${{ github.head_ref || github.ref }}
cancel-in-progress: true

permissions:
contents: read

jobs:
test-connectors-demo:
name: Test Connectors Demo Agent
runs-on: ubuntu-latest
if: github.event_name != 'pull_request' || github.event.pull_request.draft == false || contains(github.event.pull_request.labels.*.name, 'ready_for_ci')

steps:
- uses: actions/checkout@v6

- name: Set up Python
uses: actions/setup-python@v6
with:
python-version: '3.12'

- name: Install uv
run: curl -LsSf https://astral.sh/uv/install.sh | sh

- name: Install dependencies
run: |
uv pip install --system -e .[dev]
# ConnectorsDemoAgent ships as the standalone wheel (#1102)
uv pip install --system -e hub/agents/python/connectors-demo

- name: Run Connectors Demo Agent Tests
run: |
python -m pytest hub/agents/python/connectors-demo/tests/ -v --tb=short
9 changes: 8 additions & 1 deletion .github/workflows/test_gaia_cli.yml
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,13 @@ jobs:
uses: ./.github/workflows/test_chat_agent.yml
if: github.event_name != 'pull_request' || github.event.pull_request.draft == false || contains(github.event.pull_request.labels.*.name, 'ready_for_ci')

# Test Connectors Demo Agent (standalone hub wheel, #1102)
test-connectors-demo:
name: Connectors Demo Agent Tests
needs: lint
uses: ./.github/workflows/test_connectors_demo.yml
if: github.event_name != 'pull_request' || github.event.pull_request.draft == false || contains(github.event.pull_request.labels.*.name, 'ready_for_ci')

# Test Security features
test-security:
name: Security Tests
Expand All @@ -84,7 +91,7 @@ jobs:
test-summary:
name: Test Summary
runs-on: ubuntu-latest
needs: [lint, unit-tests, test-windows, test-linux, test-mcp, test-code-agent, test-chat-agent, test-security]
needs: [lint, unit-tests, test-windows, test-linux, test-mcp, test-code-agent, test-chat-agent, test-connectors-demo, test-security]
# Run always except when workflow or any dependency is cancelled (e.g., by cancel-in-progress)
if: >-
${{ always() && !cancelled() &&
Expand Down
2 changes: 1 addition & 1 deletion docs/connectors/index.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,7 @@ If you skip a grant, the demo will surface an actionable error like
grants and grant <scope>`.

The agent's source —
[`src/gaia/agents/connectors_demo/agent.py`](https://github.com/amd/gaia/blob/main/src/gaia/agents/connectors_demo/agent.py)
[`hub/agents/python/connectors-demo/gaia_agent_connectors_demo/agent.py`](https://github.com/amd/gaia/blob/main/hub/agents/python/connectors-demo/gaia_agent_connectors_demo/agent.py)
— is a working reference for any custom agent that needs to call
external services.

Expand Down
2 changes: 1 addition & 1 deletion docs/guides/custom-agent.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -170,7 +170,7 @@ A connector lets users:
Declare what your agent needs by setting `REQUIRED_CONNECTORS` on the
class, and call `get_credential_sync(connector_id, agent_id, required_scopes=[...])`
from inside a tool to get a usable token. The
[`connectors-demo` agent](https://github.com/amd/gaia/blob/main/src/gaia/agents/connectors_demo/agent.py)
[`connectors-demo` agent](https://github.com/amd/gaia/blob/main/hub/agents/python/connectors-demo/gaia_agent_connectors_demo/agent.py)
is a working reference for both `oauth_pkce` (Google) and `mcp_server`
(GitHub) connectors.

Expand Down
23 changes: 23 additions & 0 deletions hub/agents/python/connectors-demo/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
# gaia-agent-connectors-demo

Standalone GAIA agent that demonstrates the connectors framework end-to-end —
it pulls real data from your connected Google account (Gmail, Calendar, Drive)
and GitHub PAT. Depends on the published `amd-gaia` framework wheel.

## Install

```bash
pip install gaia-agent-connectors-demo # from PyPI (once published)
pip install -e hub/agents/python/connectors-demo # editable, for development
```

Installing registers the `connectors-demo` agent via the `gaia.agent`
entry-point group; the GAIA registry discovers it automatically. Select it in
the Agent UI dropdown to validate your connector setup.

## Develop / test

```bash
pip install -e ".[test]"
pytest hub/agents/python/connectors-demo/tests/ -x
```
32 changes: 32 additions & 0 deletions hub/agents/python/connectors-demo/gaia-agent.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
id: connectors-demo
name: Connectors Demo
version: 0.1.0
description: "Demonstrates the connectors framework — pulls real data from your connected Google account and GitHub PAT"
author: AMD
license: MIT

category: productivity
tags: [google, gmail, github, calendar]
icon: plug
tools_count: 4

language: python
min_gaia_version: "0.20.0"
models: []

python:
entry_module: gaia_agent_connectors_demo
entry_class: ConnectorsDemoAgent
dependencies:
- "amd-gaia>=0.20.0"

requirements:
min_memory_gb: 8
platforms: [win-x64, linux-x64, darwin-arm64]

interfaces:
tui: false
cli: false
pipe: false
api_server: true
mcp_server: false
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
# Copyright(C) 2024-2026 Advanced Micro Devices, Inc. All rights reserved.
# SPDX-License-Identifier: MIT
"""GAIA Connectors Demo agent — standalone hub package.

Registers the ``connectors-demo`` agent into the GAIA registry via the
``gaia.agent`` entry-point group. Public names are re-exported lazily so
registry discovery stays cheap.
"""

__all__ = ["build_registration"]

__version__ = "0.1.0"

_LAZY = {
"ConnectorsDemoAgent": "agent",
"ConnectorsDemoAgentConfig": "agent",
}


def __getattr__(name):
if name in _LAZY:
import importlib

module = importlib.import_module(f"gaia_agent_connectors_demo.{_LAZY[name]}")
return getattr(module, name)
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")


def build_registration():
"""Return the :class:`AgentRegistration` for the connectors-demo agent."""
import dataclasses

from gaia_agent_connectors_demo.agent import (
ConnectorsDemoAgent,
ConnectorsDemoAgentConfig,
)

from gaia.agents.registry import (
AgentRegistration,
_wrap_factory_with_namespaced_id,
)

def _factory(**kwargs):
valid_fields = {f.name for f in dataclasses.fields(ConnectorsDemoAgentConfig)}
config = ConnectorsDemoAgentConfig(
**{k: v for k, v in kwargs.items() if k in valid_fields}
)
return ConnectorsDemoAgent(config=config)

# Stamp the namespaced id onto the instance so the per-agent connector
# activation filter (#1005) can match this agent's grants — the registry's
# create_agent does not inject it for entry-point agents.
factory = _wrap_factory_with_namespaced_id(_factory, "installed:connectors-demo")

return AgentRegistration(
id="connectors-demo",
name="Connectors Demo",
description=(
"Demonstrates the connectors framework — pulls real "
"data from your connected Google account and GitHub PAT."
),
source="installed",
conversation_starters=[
"What's in my inbox?",
"What's on my calendar today?",
"List my recent Drive files",
"List my GitHub repositories",
],
factory=factory,
agent_dir=None,
models=[],
required_connections=list(ConnectorsDemoAgent.REQUIRED_CONNECTORS),
namespaced_agent_id="installed:connectors-demo",
category="productivity",
tags=["google", "gmail", "github", "calendar"],
icon="plug",
tools_count=4,
)
Original file line number Diff line number Diff line change
Expand Up @@ -55,8 +55,11 @@


# Public namespace this agent uses for grant-ledger lookups. Must agree
# with the registration in ``gaia.agents.registry``.
AGENT_NAMESPACED_ID = "builtin:connectors-demo"
# with the registration's ``namespaced_agent_id`` in
# ``gaia_agent_connectors_demo.build_registration``. The agent now ships as a
# standalone hub wheel (#1102), so it is discovered as an ``installed:`` agent
# rather than a framework ``builtin:``.
AGENT_NAMESPACED_ID = "installed:connectors-demo"

# OAuth scopes the four tools need. Declared in one place so the
# REQUIRED_CONNECTORS block and the per-tool calls can't drift apart.
Expand Down
22 changes: 22 additions & 0 deletions hub/agents/python/connectors-demo/pyproject.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
[build-system]
requires = ["setuptools>=61.0"]
build-backend = "setuptools.build_meta"

[project]
name = "gaia-agent-connectors-demo"
version = "0.1.0"
description = "GAIA connectors demo agent — Google + GitHub connector showcase"
authors = [{ name = "AMD" }]
license = { text = "MIT" }
readme = "README.md"
requires-python = ">=3.10"
dependencies = ["amd-gaia>=0.20.0"]

[project.entry-points."gaia.agent"]
connectors-demo = "gaia_agent_connectors_demo:build_registration"

[project.optional-dependencies]
test = ["pytest"]

[tool.setuptools.packages.find]
include = ["gaia_agent_connectors_demo*"]
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,7 @@
from unittest.mock import patch

import httpx

from gaia.agents.connectors_demo.agent import (
from gaia_agent_connectors_demo.agent import (
AGENT_NAMESPACED_ID,
SCOPE_CALENDAR_READ,
SCOPE_DRIVE_READ,
Expand All @@ -31,6 +30,7 @@
_github_my_repos_impl,
_gmail_recent_subjects_impl,
)

from gaia.connectors.errors import (
AuthRequiredError,
ConfigurationError,
Expand Down Expand Up @@ -169,7 +169,7 @@ def test_happy_path_returns_subjects_and_senders(self):
]
with (
patch(
"gaia.agents.connectors_demo.agent._gmail_token",
"gaia_agent_connectors_demo.agent._gmail_token",
return_value="tok-xyz",
),
patch("httpx.get", side_effect=_stub_gmail_response(fake_messages)),
Expand All @@ -182,7 +182,7 @@ def test_happy_path_returns_subjects_and_senders(self):

def test_grant_failure_returns_actionable_error(self):
with patch(
"gaia.agents.connectors_demo.agent._gmail_token",
"gaia_agent_connectors_demo.agent._gmail_token",
side_effect=AuthRequiredError(
AuthRequiredError.Reason.AGENT_NOT_GRANTED,
provider="google",
Expand All @@ -199,7 +199,7 @@ def test_api_failure_returns_connector_error(self):
# Token resolves, but Gmail returns 401.
with (
patch(
"gaia.agents.connectors_demo.agent._gmail_token",
"gaia_agent_connectors_demo.agent._gmail_token",
return_value="tok",
),
patch(
Expand Down Expand Up @@ -239,7 +239,7 @@ def test_happy_path_lists_events(self):
)
with (
patch(
"gaia.agents.connectors_demo.agent._calendar_token",
"gaia_agent_connectors_demo.agent._calendar_token",
return_value="tok",
),
patch("httpx.get", return_value=fake_response),
Expand Down Expand Up @@ -276,7 +276,7 @@ def test_happy_path_lists_files(self):
)
with (
patch(
"gaia.agents.connectors_demo.agent._drive_token",
"gaia_agent_connectors_demo.agent._drive_token",
return_value="tok",
),
patch("httpx.get", return_value=fake_response),
Expand Down Expand Up @@ -307,7 +307,7 @@ def test_happy_path_lists_repos(self):
)
with (
patch(
"gaia.agents.connectors_demo.agent._github_pat",
"gaia_agent_connectors_demo.agent._github_pat",
return_value="ghp_x",
),
patch("httpx.get", return_value=fake_response),
Expand All @@ -318,7 +318,7 @@ def test_happy_path_lists_repos(self):

def test_pat_missing_returns_connector_error(self):
with patch(
"gaia.agents.connectors_demo.agent._github_pat",
"gaia_agent_connectors_demo.agent._github_pat",
side_effect=ConnectorsError(
"GitHub MCP credential resolved but GITHUB_TOKEN was empty."
),
Expand Down Expand Up @@ -382,22 +382,22 @@ def test_each_tool_impl_returns_json_serializable(self):
# If a future change makes a dict non-serializable (e.g. nested
# datetime), this test catches it before it ships.
with patch(
"gaia.agents.connectors_demo.agent._gmail_token",
"gaia_agent_connectors_demo.agent._gmail_token",
side_effect=ConnectorsError("offline"),
):
assert json.dumps(_gmail_recent_subjects_impl(limit=1))
with patch(
"gaia.agents.connectors_demo.agent._calendar_token",
"gaia_agent_connectors_demo.agent._calendar_token",
side_effect=ConnectorsError("offline"),
):
assert json.dumps(_calendar_today_impl())
with patch(
"gaia.agents.connectors_demo.agent._drive_token",
"gaia_agent_connectors_demo.agent._drive_token",
side_effect=ConnectorsError("offline"),
):
assert json.dumps(_drive_recent_files_impl(limit=1))
with patch(
"gaia.agents.connectors_demo.agent._github_pat",
"gaia_agent_connectors_demo.agent._github_pat",
side_effect=ConnectorsError("offline"),
):
assert json.dumps(_github_my_repos_impl(limit=1))
Loading
Loading