Skip to content

Commit d71565f

Browse files
kovtcharov-amdOvtcharovitomek
authored
refactor(agents): migrate email to hub (#1102) (#1520)
## Why this matters EmailTriageAgent was the last registry *builtin* still living in the core source tree. It now ships as the standalone `gaia-agent-email` wheel under `hub/agents/python/email/`, discovered via the `gaia.agent` entry point — so the core framework wheel no longer hardcodes it and it versions independently. This completes the chat-family/email wave of the #1102 framework-only-core restructure. Unlike the analyst/browser migration (#1446), email owned a core API/MCP surface (`email_routes.py`, `email_mcp.py`). Per the restructure spec these move **into the wheel** (`gaia_agent_email/api_routes.py`, `mcp_server.py`); `openai_server.py` mounts the email router conditionally and logs an actionable `pip install gaia-agent-email` hint when the wheel is absent (no silent fallback). `gaia email` resolves through the registry and fails loudly with the same hint. ## Test plan - [x] `python util/lint.py --black --isort` — clean - [x] email package parses; no residual `gaia.agents.email` / `gaia.api.email_routes` / `email_mcp` references in `src/` - [ ] CI: new `Email Agent Unit Tests` workflow installs the wheel and runs its suite - [ ] CI: core unit + API + CLI integration green without the wheel installed (guarded mount) - [ ] Reviewer: confirm Google-connector requirement, conversation starters, and local-inference (NPU/FLM) behavior preserved Note: API/MCP surface relocation is the highest-risk area — review the guarded import in `openai_server.py` and the test `importorskip` guards. Do not merge until CI is green and reviewed. --------- Co-authored-by: Ovtcharov <kovtchar@amd.com> Co-authored-by: Tomasz Iniewicz <itomek@users.noreply.github.com>
1 parent 2a0685e commit d71565f

81 files changed

Lines changed: 2068 additions & 1562 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
# Copyright(C) 2025-2026 Advanced Micro Devices, Inc. All rights reserved.
2+
# SPDX-License-Identifier: MIT
3+
4+
# Tests the GAIA Email agent, which ships as the standalone gaia-agent-email
5+
# wheel (#1102).
6+
7+
name: Email Agent Tests
8+
9+
on:
10+
workflow_call:
11+
push:
12+
branches: [ main ]
13+
paths:
14+
- 'hub/agents/python/email/**'
15+
- 'src/gaia/agents/base/**'
16+
- 'src/gaia/agents/tools/**'
17+
- 'setup.py'
18+
- '.github/workflows/test_email_agent.yml'
19+
pull_request:
20+
branches: [ main ]
21+
types: [opened, synchronize, reopened, ready_for_review]
22+
paths:
23+
- 'hub/agents/python/email/**'
24+
- 'src/gaia/agents/base/**'
25+
- 'src/gaia/agents/tools/**'
26+
- 'setup.py'
27+
- '.github/workflows/test_email_agent.yml'
28+
merge_group:
29+
workflow_dispatch:
30+
31+
concurrency:
32+
group: ${{ github.workflow }}-${{ github.head_ref || github.ref }}
33+
cancel-in-progress: true
34+
35+
permissions:
36+
contents: read
37+
38+
jobs:
39+
test-email-agent:
40+
name: Test Email Agent
41+
runs-on: ubuntu-latest
42+
if: github.event_name != 'pull_request' || github.event.pull_request.draft == false || contains(github.event.pull_request.labels.*.name, 'ready_for_ci')
43+
44+
steps:
45+
- uses: actions/checkout@v6
46+
47+
- name: Set up Python
48+
uses: actions/setup-python@v6
49+
with:
50+
python-version: '3.12'
51+
52+
- name: Install uv
53+
run: curl -LsSf https://astral.sh/uv/install.sh | sh
54+
55+
- name: Install dependencies
56+
run: |
57+
# [api] adds fastapi/uvicorn — the email REST-spec tests
58+
# (test_spec_html.py) import the contract's api_routes module.
59+
uv pip install --system -e .[dev,api]
60+
# EmailTriageAgent ships as the standalone gaia-agent-email wheel (#1102)
61+
uv pip install --system -e hub/agents/python/email
62+
63+
- name: Run Email Agent Tests
64+
run: |
65+
# Hub package smoke tests + the repo-side email unit suite, which
66+
# importorskips without the wheel and runs here with it installed.
67+
python -m pytest \
68+
hub/agents/python/email/tests/ \
69+
tests/unit/agents/email/ \
70+
tests/unit/email/ \
71+
tests/unit/agents/test_email_agent.py \
72+
tests/unit/agents/test_email_agent_confirmation.py \
73+
tests/unit/agents/test_email_agent_local_llm_enforcement.py \
74+
tests/unit/agents/test_email_agent_prompt_injection.py \
75+
tests/unit/agents/test_email_agent_soft_delete.py \
76+
tests/unit/agents/test_email_agent_tools.py \
77+
tests/unit/agents/test_email_llm_triage.py \
78+
tests/unit/test_email_cli.py \
79+
tests/integration/test_never_auto_send.py \
80+
tests/integration/test_email_rest_api_e2e.py \
81+
tests/test_api.py::TestEmailTriageEndpoint \
82+
tests/test_api.py::TestEmailSendConfirmationGate \
83+
-v --tb=short

.github/workflows/test_gaia_cli.yml

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,13 @@ jobs:
9494
uses: ./.github/workflows/test_browser_agent.yml
9595
if: github.event_name != 'pull_request' || github.event.pull_request.draft == false || contains(github.event.pull_request.labels.*.name, 'ready_for_ci')
9696

97+
# Test Email Agent (standalone hub wheel, #1102)
98+
test-email-agent:
99+
name: Email Agent Tests
100+
needs: lint
101+
uses: ./.github/workflows/test_email_agent.yml
102+
if: github.event_name != 'pull_request' || github.event.pull_request.draft == false || contains(github.event.pull_request.labels.*.name, 'ready_for_ci')
103+
97104
# Test Security features
98105
test-security:
99106
name: Security Tests
@@ -105,7 +112,7 @@ jobs:
105112
test-summary:
106113
name: Test Summary
107114
runs-on: ubuntu-latest
108-
needs: [lint, unit-tests, test-windows, test-linux, test-mcp, test-code-agent, test-chat-agent, test-connectors-demo, test-analyst-agent, test-browser-agent, test-security]
115+
needs: [lint, unit-tests, test-windows, test-linux, test-mcp, test-code-agent, test-chat-agent, test-connectors-demo, test-analyst-agent, test-browser-agent, test-email-agent, test-security]
109116
# Run always except when workflow or any dependency is cancelled (e.g., by cancel-in-progress)
110117
if: >-
111118
${{ always() && !cancelled() &&

docs/sdk/infrastructure/connectors.mdx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -106,7 +106,7 @@ summary = conn.import_forwarded_connection(
106106
"https://www.googleapis.com/auth/calendar.events",
107107
],
108108
account_email="alice@example.com", # display-only in v1
109-
grant_agents=["builtin:email"], # agents that may use it
109+
grant_agents=["installed:email"], # agents that may use it
110110
)
111111
# summary carries metadata only — never the refresh token or client secret.
112112
```
@@ -128,7 +128,7 @@ curl -X POST http://localhost:4200/v1/connections/google \
128128
"https://www.googleapis.com/auth/gmail.send",
129129
"https://www.googleapis.com/auth/calendar.events"],
130130
"account_email": "alice@example.com",
131-
"grant_agents": ["builtin:email"]
131+
"grant_agents": ["installed:email"]
132132
}'
133133
```
134134

docs/spec/email-contract.mdx

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,12 @@ title: "Email Triage Contract Schema"
33
---
44

55
<Info>
6-
**Source Code:** [`src/gaia/agents/email/contract.py`](https://github.com/amd/gaia/blob/main/src/gaia/agents/email/contract.py)
6+
**Source Code:** [`hub/agents/python/email/gaia_agent_email/contract.py`](https://github.com/amd/gaia/blob/main/hub/agents/python/email/gaia_agent_email/contract.py)
77
</Info>
88

99
<Note>
1010
**Component:** Email request/response contract (issue #1262)
11-
**Module:** `gaia.agents.email.contract`
11+
**Module:** `gaia_agent_email.contract`
1212
**Validation:** pydantic v2
1313
**Schema version:** `1.0`
1414
</Note>
@@ -30,7 +30,7 @@ stable shape.
3030
models, guaranteeing identical structured output for a fixed input.
3131
- **Single email *and* full thread.** The input is a discriminated union on a
3232
`kind` field (`"single"` / `"thread"`); a consumer branches deterministically.
33-
- **Dependency-light.** `gaia.agents.email.contract` imports only pydantic — no
33+
- **Dependency-light.** `gaia_agent_email.contract` imports only pydantic — no
3434
Gmail or connector backends — so either surface can import it without pulling
3535
live-mail machinery into the process. (A regression test enforces this.)
3636
- **Fail loudly.** Every model forbids unknown fields (`extra="forbid"`). An
@@ -243,7 +243,7 @@ Validate a payload at a boundary (REST endpoint, MCP tool handler). Both helpers
243243
raise loudly on a contract violation — never return a partial object:
244244

245245
```python
246-
from gaia.agents.email.contract import parse_request, parse_response
246+
from gaia_agent_email.contract import parse_request, parse_response
247247

248248
request = parse_request(raw_request_dict) # -> EmailTriageRequest
249249
if request.payload.kind == "thread":

hub/agents/python/email/README.md

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
# gaia-agent-email
2+
3+
Standalone GAIA agent — read, triage, organize, and reply to email through your
4+
connected Google (Gmail) or personal Microsoft (Outlook.com) account. All email
5+
content is processed locally on Lemonade — no cloud inference. Depends on the
6+
published `amd-gaia` framework wheel.
7+
8+
## Install
9+
10+
```bash
11+
pip install gaia-agent-email # from PyPI (once published)
12+
pip install -e hub/agents/python/email # editable, for development
13+
```
14+
15+
Installing registers the `email` agent via the `gaia.agent` entry-point
16+
group; the GAIA registry discovers it automatically. The agent ships its own
17+
REST surface (`gaia_agent_email.api_routes`) and MCP stdio server
18+
(`gaia_agent_email.mcp_server`).
19+
20+
## Usage
21+
22+
```bash
23+
gaia email "Triage my inbox" # one-shot query
24+
gaia email --interactive # interactive session
25+
gaia email --spec # write + open the REST endpoint spec
26+
```
27+
28+
Requires the Google (or Microsoft) connector — connect it once with
29+
`gaia connectors` so the agent is grant-checked for mailbox access.
30+
31+
## Develop / test
32+
33+
```bash
34+
pip install -e ".[test]"
35+
pytest hub/agents/python/email/tests/ -x
36+
```
37+
38+
## License
39+
40+
Copyright(C) 2025-2026 Advanced Micro Devices, Inc. All rights reserved.
41+
42+
SPDX-License-Identifier: MIT
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
id: email
2+
name: Email Triage
3+
version: 0.1.0
4+
description: "GAIA email triage agent — read, triage, organize, and reply to Gmail/Outlook locally"
5+
author: AMD
6+
license: MIT
7+
8+
category: productivity
9+
tags: [email, gmail, calendar, triage]
10+
icon: mail
11+
tools_count: 6
12+
13+
language: python
14+
min_gaia_version: "0.20.0"
15+
models: [Gemma-4-E4B-it-GGUF]
16+
17+
python:
18+
entry_module: gaia_agent_email
19+
entry_class: EmailTriageAgent
20+
dependencies:
21+
- "amd-gaia>=0.20.0"
22+
23+
requirements:
24+
min_memory_gb: 8
25+
platforms: [win-x64, linux-x64, darwin-arm64]
26+
27+
interfaces:
28+
tui: false
29+
cli: true
30+
pipe: true
31+
api_server: true
32+
mcp_server: true
Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
# Copyright(C) 2025-2026 Advanced Micro Devices, Inc. All rights reserved.
2+
# SPDX-License-Identifier: MIT
3+
"""GAIA Email Triage agent — standalone hub package.
4+
5+
Registers the ``email`` agent into the GAIA registry via the ``gaia.agent``
6+
entry-point group (#1102). ``EmailTriageAgent`` / ``EmailAgentConfig`` are
7+
re-exported lazily (PEP 562) so that ``from gaia_agent_email.contract import
8+
...`` — the dependency-light request/response contract used by the REST surface
9+
(#1229) and the MCP stdio interface (#1104) — and registry discovery do NOT
10+
drag the agent and its Gmail / connector backends into the importing process.
11+
"""
12+
13+
from typing import TYPE_CHECKING
14+
15+
if TYPE_CHECKING: # pragma: no cover - import only for type checkers
16+
from gaia_agent_email.agent import EmailTriageAgent
17+
from gaia_agent_email.config import EmailAgentConfig
18+
19+
__all__ = ["build_registration", "EmailTriageAgent", "EmailAgentConfig"]
20+
21+
__version__ = "0.1.0"
22+
23+
_LAZY = {
24+
"EmailTriageAgent": "agent",
25+
"EmailAgentConfig": "config",
26+
}
27+
28+
29+
def __getattr__(name: str):
30+
# PEP 562 lazy attribute access — keeps the heavy agent import off the path
31+
# of dependency-light consumers (the contract module) and registry discovery.
32+
if name in _LAZY:
33+
import importlib
34+
35+
module = importlib.import_module(f"gaia_agent_email.{_LAZY[name]}")
36+
return getattr(module, name)
37+
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
38+
39+
40+
def build_registration():
41+
"""Return the :class:`AgentRegistration` for the ``email`` agent.
42+
43+
Metadata is declared with literals (and connector requirements rebuilt from
44+
the dependency-light scope constants) so discovery stays cheap — the agent
45+
and its backends are imported only when an email agent is actually created.
46+
Values mirror ``EmailTriageAgent``'s class attributes exactly.
47+
"""
48+
import dataclasses
49+
50+
from gaia.agents.registry import (
51+
AgentRegistration,
52+
_wrap_factory_with_namespaced_id,
53+
)
54+
from gaia.connectors.providers.base import ConnectorRequirement
55+
56+
from gaia_agent_email.outlook_scopes import (
57+
OUTLOOK_CALENDAR_SCOPES,
58+
OUTLOOK_MAIL_SCOPES,
59+
)
60+
from gaia_agent_email.scopes import ALL_SCOPES
61+
62+
def email_factory(**kwargs):
63+
from gaia_agent_email.agent import EmailTriageAgent
64+
from gaia_agent_email.config import EmailAgentConfig
65+
66+
valid_fields = {f.name for f in dataclasses.fields(EmailAgentConfig)}
67+
config = EmailAgentConfig(
68+
**{k: v for k, v in kwargs.items() if k in valid_fields}
69+
)
70+
return EmailTriageAgent(config=config)
71+
72+
# Provider-superset connector list, mirrored from
73+
# EmailTriageAgent.REQUIRED_CONNECTORS so the AgentUI offers both the Google
74+
# and Microsoft tiles. Rebuilt from the light scope constants to avoid
75+
# importing the heavy agent module at discovery time.
76+
required_connections = [
77+
ConnectorRequirement(
78+
connector_id="google",
79+
scopes=ALL_SCOPES,
80+
reason=(
81+
"Read and organize Gmail messages, send drafts on your "
82+
"behalf, and respond to Google Calendar invites."
83+
),
84+
),
85+
ConnectorRequirement(
86+
connector_id="microsoft",
87+
scopes=OUTLOOK_MAIL_SCOPES + OUTLOOK_CALENDAR_SCOPES,
88+
reason=(
89+
"Read and organize your personal Outlook.com mailbox, send "
90+
"messages on your behalf, and read/respond to your Outlook "
91+
"calendar via Microsoft Graph."
92+
),
93+
),
94+
]
95+
96+
return AgentRegistration(
97+
id="email",
98+
name="Email Triage",
99+
description=(
100+
"Read, triage, organize, and reply to email through your "
101+
"connected Google account. All email content is processed "
102+
"locally on your machine."
103+
),
104+
source="installed",
105+
conversation_starters=[
106+
"Run a pre-scan",
107+
"Triage my inbox",
108+
"Summarize my unread emails",
109+
"Draft a reply to my most recent message",
110+
"Show me today's calendar",
111+
],
112+
factory=_wrap_factory_with_namespaced_id(email_factory, "installed:email"),
113+
agent_dir=None,
114+
models=[],
115+
required_connections=required_connections,
116+
namespaced_agent_id="installed:email",
117+
category="productivity",
118+
tags=["email", "gmail", "calendar", "triage"],
119+
icon="mail",
120+
tools_count=6,
121+
)
File renamed without changes.

0 commit comments

Comments
 (0)