|
|
4 days ago | |
|---|---|---|
| app | 5 days ago | |
| docs | 4 days ago | |
| tests | 5 days ago | |
| typings | 7 days ago | |
| .DS_Store | 1 week ago | |
| .dockerignore | 7 days ago | |
| .env.example | 5 days ago | |
| .gitignore | 1 week ago | |
| Dockerfile | 6 days ago | |
| README.md | 5 days ago | |
| docker-compose.yml | 6 days ago | |
| pyproject.toml | 5 days ago | |
| simple-agent-google-credentials.json | 1 week ago | |
| uv.lock | 5 days ago | |
README.md
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
LinkedInlabel/folder - moves advertising emails to an
Advertisinglabel/folder - moves veille techno emails to a
VeilleTechnolabel/folder - scans the
Advertisinglabel 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)- 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
- In Google Cloud Console, create a desktop OAuth client.
- Download the client JSON file.
- 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
uv sync
cp .env.example .env
Edit .env and set:
AGENT_API_KEYto a strong secret for agent-to-agent callsAUTH_MODE(api_key,jwt, orhybrid)- optional JWT settings (
AUTH_JWT_SECRET,AUTH_JWT_ISSUER,AUTH_JWT_AUDIENCE) STRANDS_OPENAI_API_KEYand optionalSTRANDS_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_RESOURCE_SERVER_URL,MCP_OAUTH_INTROSPECTION_URL, optionalMCP_OAUTH_CLIENT_ID/MCP_OAUTH_CLIENT_SECRET, and optionalMCP_OAUTH_ISSUER/MCP_OAUTH_AUDIENCE
4) Run
Local
uv run uvicorn app.main:app --reload
Docker
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) whenUNSUBSCRIBE_AUTO_ENABLED=true
5) API usage
Health check
curl http://127.0.0.1:8000/health
Manual Gmail scan
curl -X POST "http://127.0.0.1:8000/scan?max_results=100" \
-H "X-API-Key: your-secret"
Availability for other AI agents
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.
Suggested meeting intervals for AI agents
curl -X POST "http://127.0.0.1:8000/meeting-intervals" \
-H "Content-Type: application/json" \
-H "X-API-Key: your-secret" \
-d '{
"start": "2026-03-09T08:00:00+01:00",
"end": "2026-03-10T18:00:00+01:00",
"calendar_ids": ["primary"]
}'
A2A discovery
curl http://127.0.0.1:8000/.well-known/agent-card.json
A2A SendMessage availability
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"]
}
}'
A2A SendMessage meeting intervals
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-2",
"method": "SendMessage",
"params": {
"action": "available_meeting_intervals",
"start": "2026-03-09T08:00:00+01:00",
"end": "2026-03-10T18:00:00+01:00",
"calendar_ids": ["primary"]
}
}'
MCP server (availability and scheduling tools)
Run MCP on a dedicated port:
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:
http://127.0.0.1:8001/mcp
By default, MCP exposes check_availability and available_meeting_intervals.
To expose internal mutation tools (scan_mailbox, list_unsubscribe_candidates, execute_unsubscribe), set:
MCP_ENABLE_MUTATION_TOOLS=true
MCP auth mode defaults to inherit (same mode as AUTH_MODE).
To force OAuth only on MCP:
MCP_AUTH_MODE=oauth
MCP_RESOURCE_SERVER_URL=https://mcp.example.com/mcp
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:readavailable_meeting_intervals:available_meeting_intervals:readscan_mailbox:mail:scanlist_unsubscribe_candidates:unsubscribe:readexecute_unsubscribe:unsubscribe:execute
With MCP_AUTH_MODE=oauth, the MCP server also exposes RFC 9728 protected-resource metadata:
https://<host>/.well-known/oauth-protected-resource/mcp
Manual unsubscribe digest
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
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
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)
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_QUERYis 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=trueonly if you want rules-based backup when LLM calls fail. - Every scanned message gets an
AgentProcessedlabel to avoid reprocessing loops.
Unsubscribe digest behavior
- Reads emails from
UNSUBSCRIBE_QUERY(defaultlabel:Advertising). - Extracts unsubscribe URLs from
List-Unsubscribeheaders 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_idvalues 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:
LinkedInVeilleTechnoAdvertisingAgentProcessed
- Messages classified as LinkedIn/Advertising/Veille_Techno are removed from
INBOX(moved out of inbox).