You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
254 lines
7.4 KiB
Markdown
254 lines
7.4 KiB
Markdown
# Personal Gmail + Calendar Agent
|
|
|
|
This project runs a small local API service that:
|
|
|
|
- scans unread emails in the root Gmail inbox
|
|
- classifies emails with **Strands** (`LINKEDIN`, `ADVERTISING`, `VEILLE_TECHNO`, `OTHER`)
|
|
- moves LinkedIn emails to a `LinkedIn` label/folder
|
|
- moves advertising emails to an `Advertising` label/folder
|
|
- moves veille techno emails to a `VeilleTechno` label/folder
|
|
- scans the `Advertising` label and emails you new unsubscribe links (deduplicated)
|
|
- discovers unsubscribe-ready mailing lists for human review, then auto-unsubscribes selected lists
|
|
- exposes a secure availability endpoint powered by Google Calendar free/busy
|
|
|
|
## 1) Prerequisites
|
|
|
|
- Python 3.11+
|
|
- `uv` ([installation guide](https://docs.astral.sh/uv/getting-started/installation/))
|
|
- A Google account
|
|
- An OpenAI-compatible API key for Strands (`STRANDS_OPENAI_API_KEY`)
|
|
- A Google Cloud project with:
|
|
- Gmail API enabled
|
|
- Google Calendar API enabled
|
|
- OAuth consent configured
|
|
- OAuth Client ID of type **Desktop app**
|
|
|
|
## 2) Google OAuth setup
|
|
|
|
1. In Google Cloud Console, create a desktop OAuth client.
|
|
2. Download the client JSON file.
|
|
3. Save it in this project as `credentials.json`.
|
|
|
|
The first run opens a browser window for consent and creates `token.json`.
|
|
If your existing token was created before `gmail.send` was added, you may be prompted again.
|
|
|
|
## 3) Install and configure
|
|
|
|
```bash
|
|
uv sync
|
|
cp .env.example .env
|
|
```
|
|
|
|
Edit `.env` and set:
|
|
|
|
- `AGENT_API_KEY` to a strong secret for agent-to-agent calls
|
|
- `AUTH_MODE` (`api_key`, `jwt`, or `hybrid`)
|
|
- optional JWT settings (`AUTH_JWT_SECRET`, `AUTH_JWT_ISSUER`, `AUTH_JWT_AUDIENCE`)
|
|
- `STRANDS_OPENAI_API_KEY` and optional `STRANDS_MODEL_ID` / `STRANDS_OPENAI_BASE_URL`
|
|
- optional unsubscribe digest settings (`UNSUBSCRIBE_*`)
|
|
- optional scan frequency and additional Gmail filters (`GMAIL_QUERY`)
|
|
- optional MCP auth override (`MCP_AUTH_MODE=inherit|api_key|jwt|hybrid|oauth`)
|
|
- for MCP OAuth mode: `MCP_OAUTH_INTROSPECTION_URL`, optional `MCP_OAUTH_CLIENT_ID` / `MCP_OAUTH_CLIENT_SECRET`, and optional `MCP_OAUTH_ISSUER` / `MCP_OAUTH_AUDIENCE`
|
|
|
|
## 4) Run
|
|
|
|
### Local
|
|
|
|
```bash
|
|
uv run uvicorn app.main:app --reload
|
|
```
|
|
|
|
### Docker
|
|
|
|
```bash
|
|
docker compose up --build -d personal-agent personal-agent-mcp
|
|
docker compose logs -f personal-agent personal-agent-mcp
|
|
```
|
|
|
|
Docker endpoints:
|
|
|
|
- REST + A2A API: `http://127.0.0.1:8000`
|
|
- MCP streamable HTTP: `http://127.0.0.1:8001/mcp`
|
|
|
|
Container mounts:
|
|
|
|
- `./${GOOGLE_CLIENT_SECRETS_FILE}` -> `/app/${GOOGLE_CLIENT_SECRETS_FILE}` (read-only)
|
|
- `./${GOOGLE_TOKEN_FILE}` -> `/app/${GOOGLE_TOKEN_FILE}`
|
|
- `./data` -> `/app/data`
|
|
|
|
At startup, the scheduler runs:
|
|
|
|
- Gmail triage (`GMAIL_SCAN_INTERVAL_MINUTES`)
|
|
- unsubscribe digest (`UNSUBSCRIBE_DIGEST_INTERVAL_MINUTES`)
|
|
- auto-unsubscribe for approved lists (`UNSUBSCRIBE_AUTO_INTERVAL_MINUTES`) when `UNSUBSCRIBE_AUTO_ENABLED=true`
|
|
|
|
## 5) API usage
|
|
|
|
### Health check
|
|
|
|
```bash
|
|
curl http://127.0.0.1:8000/health
|
|
```
|
|
|
|
### Manual Gmail scan
|
|
|
|
```bash
|
|
curl -X POST "http://127.0.0.1:8000/scan?max_results=100" \
|
|
-H "X-API-Key: your-secret"
|
|
```
|
|
|
|
### Availability for other AI agents
|
|
|
|
```bash
|
|
curl -X POST "http://127.0.0.1:8000/availability" \
|
|
-H "Content-Type: application/json" \
|
|
-H "X-API-Key: your-secret" \
|
|
-d '{
|
|
"start": "2026-03-09T09:00:00+01:00",
|
|
"end": "2026-03-09T10:00:00+01:00",
|
|
"calendar_ids": ["primary"]
|
|
}'
|
|
```
|
|
|
|
If `available` is `true`, there are no busy slots in that range.
|
|
|
|
### A2A discovery
|
|
|
|
```bash
|
|
curl http://127.0.0.1:8000/.well-known/agent-card.json
|
|
```
|
|
|
|
### A2A SendMessage availability
|
|
|
|
```bash
|
|
curl -X POST "http://127.0.0.1:8000/a2a/rpc" \
|
|
-H "Content-Type: application/json" \
|
|
-H "X-API-Key: your-secret" \
|
|
-d '{
|
|
"jsonrpc": "2.0",
|
|
"id": "req-1",
|
|
"method": "SendMessage",
|
|
"params": {
|
|
"start": "2026-03-09T09:00:00+01:00",
|
|
"end": "2026-03-09T10:00:00+01:00",
|
|
"calendar_ids": ["primary"]
|
|
}
|
|
}'
|
|
```
|
|
|
|
### MCP server (availability tool)
|
|
|
|
Run MCP on a dedicated port:
|
|
|
|
```bash
|
|
uv run uvicorn app.mcp_main:app --host 0.0.0.0 --port 8001
|
|
```
|
|
|
|
With Docker Compose, MCP is provided by the `personal-agent-mcp` service.
|
|
|
|
MCP streamable HTTP endpoint:
|
|
|
|
```text
|
|
http://127.0.0.1:8001/mcp
|
|
```
|
|
|
|
By default, MCP exposes only `check_availability`.
|
|
To expose internal mutation tools (`scan_mailbox`, `list_unsubscribe_candidates`, `execute_unsubscribe`), set:
|
|
|
|
```bash
|
|
MCP_ENABLE_MUTATION_TOOLS=true
|
|
```
|
|
|
|
MCP auth mode defaults to `inherit` (same mode as `AUTH_MODE`).
|
|
To force OAuth only on MCP:
|
|
|
|
```bash
|
|
MCP_AUTH_MODE=oauth
|
|
MCP_OAUTH_INTROSPECTION_URL=https://issuer.example/oauth2/introspect
|
|
MCP_OAUTH_CLIENT_ID=your-resource-server-client-id
|
|
MCP_OAUTH_CLIENT_SECRET=your-resource-server-client-secret
|
|
MCP_OAUTH_ISSUER=https://issuer.example
|
|
MCP_OAUTH_AUDIENCE=personal-agent-mcp
|
|
```
|
|
|
|
Scopes required per MCP tool:
|
|
|
|
- `check_availability`: `availability:read`
|
|
- `scan_mailbox`: `mail:scan`
|
|
- `list_unsubscribe_candidates`: `unsubscribe:read`
|
|
- `execute_unsubscribe`: `unsubscribe:execute`
|
|
|
|
### Manual unsubscribe digest
|
|
|
|
```bash
|
|
curl -X POST "http://127.0.0.1:8000/unsubscribe-digest?max_results=500" \
|
|
-H "X-API-Key: your-secret"
|
|
```
|
|
|
|
### Human-in-the-loop unsubscribe candidates
|
|
|
|
```bash
|
|
curl -X POST "http://127.0.0.1:8000/unsubscribe/candidates?max_results=500" \
|
|
-H "X-API-Key: your-secret"
|
|
```
|
|
|
|
### Execute unsubscribe for selected mailing lists
|
|
|
|
```bash
|
|
curl -X POST "http://127.0.0.1:8000/unsubscribe/execute" \
|
|
-H "Content-Type: application/json" \
|
|
-H "X-API-Key: your-secret" \
|
|
-d '{
|
|
"selected_candidate_ids": ["abc123def456", "987zyx654wvu"],
|
|
"remember_selection": true
|
|
}'
|
|
```
|
|
|
|
### Trigger auto-unsubscribe run (approved lists only)
|
|
|
|
```bash
|
|
curl -X POST "http://127.0.0.1:8000/unsubscribe/auto-run?max_results=500" \
|
|
-H "X-API-Key: your-secret"
|
|
```
|
|
|
|
## Classification behavior
|
|
|
|
- Scan scope is always forced to `in:inbox is:unread` (root inbox + unread).
|
|
- `GMAIL_QUERY` is treated as additional filters (for example `-label:AgentProcessed`).
|
|
- Strands classification is used for each email (`LINKEDIN`, `ADVERTISING`, `VEILLE_TECHNO`, `OTHER`).
|
|
- LinkedIn has priority over advertising inside the classifier prompt.
|
|
- Set `LLM_FALLBACK_TO_RULES=true` only if you want rules-based backup when LLM calls fail.
|
|
- Every scanned message gets an `AgentProcessed` label to avoid reprocessing loops.
|
|
|
|
## Unsubscribe digest behavior
|
|
|
|
- Reads emails from `UNSUBSCRIBE_QUERY` (default `label:Advertising`).
|
|
- Extracts unsubscribe URLs from `List-Unsubscribe` headers and message content.
|
|
- Removes duplicates within the run and across runs.
|
|
- Persists already sent links in `UNSUBSCRIBE_STATE_FILE`.
|
|
- Sends only new links by email, unless `UNSUBSCRIBE_SEND_EMPTY_DIGEST=true`.
|
|
|
|
## Human-In-The-Loop Unsubscribe
|
|
|
|
- Candidate discovery groups advertising messages into mailing lists using headers (`List-Id`, `From`) and unsubscribe methods.
|
|
- You pick the `candidate_id` values you want to unsubscribe from.
|
|
- Only selected lists are executed.
|
|
- Executed unsubscribe methods are persisted and never executed twice.
|
|
- Selected lists can be remembered (`remember_selection=true`) for scheduled auto-unsubscribe.
|
|
- State is saved in `UNSUBSCRIBE_HIL_STATE_FILE`.
|
|
|
|
## Runbooks
|
|
|
|
- A2A protocol runbook: `docs/a2a.md`
|
|
- MCP protocol runbook: `docs/mcp.md`
|
|
- Security, rotation, and rollout checklist: `docs/security.md`
|
|
|
|
## Notes
|
|
|
|
- Gmail "folders" are labels. This agent creates:
|
|
- `LinkedIn`
|
|
- `VeilleTechno`
|
|
- `Advertising`
|
|
- `AgentProcessed`
|
|
- Messages classified as LinkedIn/Advertising/Veille_Techno are removed from `INBOX` (moved out of inbox).
|