diff --git a/mcp/delegation/README.md b/mcp/delegation/README.md new file mode 100644 index 0000000..b28cd29 --- /dev/null +++ b/mcp/delegation/README.md @@ -0,0 +1,53 @@ +# Model Delegation Helper + +Spawns lower-tier Claude models via Claude CLI using your subscription. + +## Setup + +**No API key needed!** Uses Claude CLI with your existing subscription. + +Requirements: +- Claude CLI at `/home/linuxbrew/.linuxbrew/bin/claude` +- Active Claude subscription (Pro/Max) + +## Usage + +### Gmail operations (tiered) +```bash +PY=~/.claude/mcp/gmail/venv/bin/python +HELPER=~/.claude/mcp/delegation/gmail_delegate.py + +# Haiku tier - just fetch and list (no LLM call) +$PY $HELPER check-unread --days 7 + +# Sonnet tier - fetch + summarize (uses claude CLI) +$PY $HELPER summarize --query "from:github.com" + +# Sonnet tier - urgent triage (uses claude CLI) +$PY $HELPER urgent +``` + +## Model Tiers + +| Tier | Model | LLM Call | Use For | +|------|-------|----------|---------| +| haiku | (none - just fetch) | No | List, count, group emails | +| sonnet | claude --model sonnet | Yes | Summarize, categorize, triage | +| opus | (PA direct) | N/A | Complex reasoning, prioritization | + +## How It Works + +1. **check-unread**: Fetches emails via Gmail API, groups by sender. No LLM needed. +2. **summarize**: Fetches emails, then spawns `claude --print --model sonnet` to summarize. +3. **urgent**: Fetches flagged emails, spawns Sonnet to triage by urgency. + +## Integration with PA + +The Personal Assistant (Opus) delegates gmail operations: + +| User Request | Delegation | Model Used | +|--------------|------------|------------| +| "Check my email" | `check-unread` | None (fetch only) | +| "Summarize X emails" | `summarize` | Sonnet via CLI | +| "What's urgent?" | `urgent` | Sonnet via CLI | +| "What should I prioritize?" | (direct) | Opus (PA) | diff --git a/mcp/delegation/gmail_delegate.py b/mcp/delegation/gmail_delegate.py new file mode 100755 index 0000000..7fb3eca --- /dev/null +++ b/mcp/delegation/gmail_delegate.py @@ -0,0 +1,286 @@ +#!/usr/bin/env python3 +""" +Gmail Delegation Helper + +Fetches emails via Gmail API, then delegates summarization to appropriate model tier. + +Usage: + gmail_delegate.py check-unread [--days N] + gmail_delegate.py summarize --query "QUERY" + gmail_delegate.py urgent +""" + +import sys +import os +import json +import argparse +import base64 +import re +from collections import defaultdict +from pathlib import Path + +# Set credentials path +os.environ["GMAIL_CREDENTIALS_PATH"] = str(Path.home() / ".gmail-mcp" / "credentials.json") + +# Note: Run this script with ~/.claude/mcp/gmail/venv/bin/python +from gmail_mcp.utils.GCP.gmail_auth import get_gmail_service +import subprocess + +# Claude CLI path +CLAUDE_CLI = "/home/linuxbrew/.linuxbrew/bin/claude" + + +def delegate(model: str, system: str, prompt: str, max_tokens: int = 4096) -> dict: + """Delegate a task to Claude CLI using subscription.""" + try: + # Build command + cmd = [ + CLAUDE_CLI, + "--print", + "--model", model, + "--system-prompt", system, + "--output-format", "json", + prompt + ] + + # Run claude CLI + result = subprocess.run( + cmd, + capture_output=True, + text=True, + timeout=120 + ) + + if result.returncode != 0: + return {"error": f"Claude CLI error: {result.stderr}"} + + # Parse JSON output + try: + output = json.loads(result.stdout) + # Extract text from the response + content = output.get("result", "") + return { + "success": True, + "model": model, + "content": content, + "usage": output.get("usage", {}) + } + except json.JSONDecodeError: + # If not JSON, use raw text output + return { + "success": True, + "model": model, + "content": result.stdout.strip(), + "usage": {} + } + + except subprocess.TimeoutExpired: + return {"error": "Claude CLI timed out"} + except FileNotFoundError: + return {"error": f"Claude CLI not found at {CLAUDE_CLI}"} + except Exception as e: + return {"error": f"Unexpected error: {e}"} + + +def fetch_emails(query: str, max_results: int = 25) -> list[dict]: + """Fetch emails matching query, return metadata + snippets.""" + service = get_gmail_service() + results = service.users().messages().list( + userId='me', q=query, maxResults=max_results + ).execute() + + emails = [] + for msg in results.get('messages', []): + detail = service.users().messages().get( + userId='me', id=msg['id'], format='metadata', + metadataHeaders=['From', 'Subject', 'Date'] + ).execute() + + headers = {h['name']: h['value'] for h in detail['payload']['headers']} + emails.append({ + 'id': msg['id'], + 'from': headers.get('From', 'Unknown'), + 'subject': headers.get('Subject', '(no subject)'), + 'date': headers.get('Date', 'Unknown'), + 'snippet': detail.get('snippet', '')[:200] + }) + + return emails + + +def fetch_email_body(msg_id: str) -> str: + """Fetch full email body for summarization.""" + service = get_gmail_service() + detail = service.users().messages().get( + userId='me', id=msg_id, format='full' + ).execute() + + payload = detail['payload'] + + def find_text(part): + if part.get('mimeType') == 'text/plain': + data = part['body'].get('data', '') + if data: + return base64.urlsafe_b64decode(data).decode('utf-8', errors='ignore') + if 'parts' in part: + for p in part['parts']: + result = find_text(p) + if result: + return result + return None + + def find_html(part): + if part.get('mimeType') == 'text/html': + data = part['body'].get('data', '') + if data: + html = base64.urlsafe_b64decode(data).decode('utf-8', errors='ignore') + # Strip HTML tags + text = re.sub(r']*>.*?', '', html, flags=re.DOTALL) + text = re.sub(r']*>.*?', '', text, flags=re.DOTALL) + text = re.sub(r'<[^>]+>', ' ', text) + text = re.sub(r' ', ' ', text) + text = re.sub(r'&', '&', text) + text = re.sub(r'\s+', ' ', text) + return text.strip() + if 'parts' in part: + for p in part['parts']: + result = find_html(p) + if result: + return result + return None + + text = find_text(payload) or find_html(payload) or detail.get('snippet', '') + return text[:3000] # Limit for context window + + +def check_unread(days: int = 7) -> dict: + """Check unread emails - Haiku tier operation.""" + query = f"is:unread newer_than:{days}d" + emails = fetch_emails(query) + + # Group by sender (simple operation, no LLM needed) + by_sender = defaultdict(list) + for email in emails: + sender = email['from'].split('<')[0].strip().strip('"') + by_sender[sender].append(email['subject'][:50]) + + return { + "tier": "haiku", + "operation": "check_unread", + "total": len(emails), + "by_sender": dict(by_sender) + } + + +def summarize_emails(query: str, max_results: int = 10) -> dict: + """Summarize emails matching query - Sonnet tier operation.""" + emails = fetch_emails(query, max_results) + + if not emails: + return {"tier": "sonnet", "operation": "summarize", "summary": "No emails found."} + + # Build context for summarization + context = "Emails to summarize:\n\n" + for i, email in enumerate(emails, 1): + body = fetch_email_body(email['id']) + context += f"--- Email {i} ---\n" + context += f"From: {email['from']}\n" + context += f"Subject: {email['subject']}\n" + context += f"Date: {email['date']}\n" + context += f"Content: {body[:1000]}\n\n" + + # Delegate to Sonnet for summarization + system = """You are an email summarization assistant. Provide concise, actionable summaries. +Focus on: key points, action items, important dates, and who needs response. +Format: Use bullet points, group by topic if multiple related emails.""" + + prompt = f"""Summarize these emails concisely: + +{context} + +Provide a brief summary highlighting what's important and any action items.""" + + result = delegate("sonnet", system, prompt, max_tokens=1024) + + return { + "tier": "sonnet", + "operation": "summarize", + "email_count": len(emails), + "summary": result.get("content", result.get("error", "Failed to summarize")), + "usage": result.get("usage", {}) + } + + +def check_urgent() -> dict: + """Check for urgent emails - Haiku fetch + Sonnet analysis.""" + query = 'is:unread newer_than:3d (subject:urgent OR subject:asap OR subject:"action required" OR is:important)' + emails = fetch_emails(query, max_results=15) + + if not emails: + return {"tier": "haiku", "operation": "urgent", "message": "No urgent emails found."} + + # For urgent, we want Sonnet to prioritize + context = "Potentially urgent emails:\n\n" + for email in emails: + context += f"- From: {email['from']}\n" + context += f" Subject: {email['subject']}\n" + context += f" Date: {email['date']}\n" + context += f" Preview: {email['snippet']}\n\n" + + system = """You are an email triage assistant. Identify truly urgent items that need immediate attention. +Distinguish between: actually urgent (needs response today), important (needs response soon), and FYI (can wait).""" + + prompt = f"""Triage these flagged emails by urgency: + +{context} + +List any that are truly urgent first, then important, then FYI.""" + + result = delegate("sonnet", system, prompt, max_tokens=1024) + + return { + "tier": "sonnet", + "operation": "urgent", + "email_count": len(emails), + "triage": result.get("content", result.get("error", "Failed to triage")), + "usage": result.get("usage", {}) + } + + +def main(): + parser = argparse.ArgumentParser(description="Gmail operations with tiered delegation") + subparsers = parser.add_subparsers(dest="command", required=True) + + # check-unread + unread = subparsers.add_parser("check-unread", help="List unread emails (Haiku tier)") + unread.add_argument("--days", "-d", type=int, default=7, help="Days to look back") + + # summarize + summarize = subparsers.add_parser("summarize", help="Summarize emails (Sonnet tier)") + summarize.add_argument("--query", "-q", required=True, help="Gmail search query") + summarize.add_argument("--max", "-m", type=int, default=10, help="Max emails to summarize") + + # urgent + subparsers.add_parser("urgent", help="Check urgent emails (Sonnet tier)") + + args = parser.parse_args() + + try: + if args.command == "check-unread": + result = check_unread(args.days) + elif args.command == "summarize": + result = summarize_emails(args.query, args.max) + elif args.command == "urgent": + result = check_urgent() + else: + result = {"error": f"Unknown command: {args.command}"} + + print(json.dumps(result, indent=2)) + + except Exception as e: + print(json.dumps({"error": str(e)})) + sys.exit(1) + + +if __name__ == "__main__": + main() diff --git a/skills/gmail/SKILL.md b/skills/gmail/SKILL.md index 719eb0f..936140b 100644 --- a/skills/gmail/SKILL.md +++ b/skills/gmail/SKILL.md @@ -9,6 +9,34 @@ allowed-tools: Access Gmail via direct Python API calls. Uses OAuth credentials at `~/.gmail-mcp/`. +## Delegated Operations (Recommended) + +Use the tiered delegation helper for cost-efficient operations. Uses Claude CLI with your subscription (no API key needed): + +```bash +GMAIL_PY=~/.claude/mcp/gmail/venv/bin/python +HELPER=~/.claude/mcp/delegation/gmail_delegate.py + +# Haiku tier - list unread (no LLM call, just fetches) +$GMAIL_PY $HELPER check-unread --days 7 + +# Sonnet tier - summarize emails (spawns claude --model sonnet) +$GMAIL_PY $HELPER summarize --query "from:github.com" + +# Sonnet tier - triage urgent (spawns claude --model sonnet) +$GMAIL_PY $HELPER urgent +``` + +### When to Use Each Tier + +| Request Type | Command | Model | +|--------------|---------|-------| +| "Check my email" | `check-unread` | Haiku | +| "How many unread?" | `check-unread` | Haiku | +| "Summarize X" | `summarize --query "X"` | Sonnet | +| "What's urgent?" | `urgent` | Sonnet | +| "What should I prioritize?" | (PA direct) | Opus | + ## Usage For any Gmail request, use Bash to run the Python helper: @@ -83,8 +111,37 @@ for msg in results.get('messages', []): EOF ``` +## Model Selection + +Gmail operations use tiered delegation per `model-policy.json`: + +| Model | Use For | Examples | +|-------|---------|----------| +| **Haiku** | Fetch, count, list, simple search | "How many unread?", "List from X" | +| **Sonnet** | Summarize, categorize, extract | "Summarize this email", "Group by topic" | +| **Opus** | Prioritize, analyze, cross-reference | "What should I handle first?" | + +### Delegation Pattern + +When invoking gmail operations, the PA should: + +1. **Classify the request** — Is it fetch-only, summarization, or analysis? +2. **Delegate appropriately**: + - Haiku: API calls + simple formatting + - Sonnet: API calls + content understanding + - Opus: Only for strategic reasoning +3. **Escalate if needed** — If lower tier can't handle it, escalate + +### Implementation Note + +Until Task tool delegation is available, the PA executes gmail operations directly +but should mentally "account" for which tier the work belongs to. This policy +enables future cost optimization when subagent spawning is implemented. + ## Policy - Read-only operations only - Summarize results (don't dump raw content) - Report metadata, not full body unless asked +- Start with lowest capable model tier +- Escalate only when task complexity requires diff --git a/state/future-considerations.json b/state/future-considerations.json index 8418e1b..758bcc1 100644 --- a/state/future-considerations.json +++ b/state/future-considerations.json @@ -1 +1 @@ -{"version":"1.0.0","description":"Deferred features and decisions for future implementation","items":[{"id":"fc-001","category":"infrastructure","title":"Workstation monitoring with Prometheus","description":"Deploy node_exporter and Alertmanager for workstation metrics and alerting","priority":"medium","status":"deferred","created":"2024-12-28","notes":"Would enable proactive alerting for disk, memory, CPU issues"},{"id":"fc-002","category":"agent","title":"Network admin agent","description":"Agent for network configuration, firewall rules, VPN management","priority":"medium","status":"deferred","created":"2024-12-28","notes":"Would manage iptables/nftables, NetworkManager, WireGuard"},{"id":"fc-003","category":"agent","title":"Personal assistant agent","description":"Agent for personal tasks, reminders, scheduling","priority":"medium","status":"deferred","created":"2024-12-28","notes":"Integration with calendar, task management"},{"id":"fc-004","category":"integration","title":"External LLM integration","description":"Support for non-Claude models in the agent system","priority":"low","status":"deferred","created":"2024-12-28","notes":"For specialized tasks or cost optimization"},{"id":"fc-005","category":"optimization","title":"Model usage logging and cost tracking","description":"Track model usage across agents for cost analysis","priority":"medium","status":"deferred","created":"2024-12-28","notes":"Would help optimize model selection policy"},{"id":"fc-006","category":"design","title":"Slash commands redesign","description":"Revisit slash command architecture and user experience","priority":"low","status":"deferred","created":"2024-12-28","notes":"Current design may need refinement"},{"id":"fc-007","category":"optimization","title":"Document structure optimization","description":"Optimize agent document format for efficiency","priority":"low","status":"deferred","created":"2024-12-28","notes":"Balance between clarity and token usage"},{"id":"fc-008","category":"infrastructure","title":"ArgoCD CLI authentication","description":"Configure argocd CLI with proper authentication","priority":"medium","status":"resolved","created":"2025-12-28","resolved":"2025-12-28","notes":"Using 10-year API token (expires 2035-12-26). Token ID: e3980c6a-1c4e-4f1a-8459-a120a5c60cc5. Stored in ~/.config/argocd/config. No renewal automation needed."},{"id":"fc-009","category":"infrastructure","title":"Prometheus local port-forward","description":"Document Prometheus access patterns for agents","priority":"low","status":"identified","created":"2025-12-28","notes":"Prometheus not accessible on localhost:9090. Options: (1) use kubectl exec to query, (2) set up port-forward, (3) use ingress. Currently works via pod exec."},{"id":"fc-010","category":"infrastructure","title":"Clone homelab gitops repo locally","description":"Clone git@github.com:will666/homelab.git for git-operator access","priority":"low","status":"resolved","created":"2025-12-28","resolved":"2025-12-28","notes":"Cloned to ~/.claude/repos/homelab"},{"id":"fc-011","category":"k8s-health","title":"Address OutOfSync ArgoCD apps","description":"5 apps OutOfSync, 1 Degraded (porthole)","priority":"medium","status":"identified","created":"2025-12-28","notes":"OutOfSync: adopt-a-street, ai-stack, gitea, home-assistant, kubernetes-dashboard, speetest-tracker. Degraded: porthole"},{"id":"fc-012","category":"agent-memory","title":"PA knowledge base with session caching","description":"Local KB for infrastructure facts with lazy-load and in-session caching","priority":"medium","status":"resolved","created":"2025-12-28","resolved":"2025-12-28","notes":"Implemented. KB files at state/kb.json (shared) and state/personal-assistant/kb.json (private). PA agent updated with lazy-load behavior."},{"id":"fc-013","category":"agent-memory","title":"Vector database for agent long-term memory","description":"Semantic search over agent knowledge using embeddings","priority":"low","status":"deferred","created":"2025-12-28","notes":"Would enable fuzzy matching, semantic queries, and scalable knowledge storage. Consider: ChromaDB, Qdrant, or pgvector."},{"id":"fc-014","category":"observability","title":"Grafana predefined reports","description":"Slash command like /grafana-report services to get standard metrics from known dashboards","priority":"low","status":"deferred","created":"2025-12-29","notes":"Requires comprehensive dashboard coverage first. Revisit when observability matures."},{"id":"fc-015","category":"observability","title":"Grafana integration in diagnostics","description":"Auto-pull Grafana dashboard data during /k8s:diagnose or health checks","priority":"low","status":"deferred","created":"2025-12-29","notes":"Would make Grafana the first troubleshooting tool. Depends on fc-016 and mature observability setup."},{"id":"fc-016","category":"observability","title":"Extend prometheus-analyst with Grafana API","description":"Add Grafana API query capability to existing prometheus-analyst agent","priority":"low","status":"deferred","created":"2025-12-29","notes":"Preferred approach over creating new agent/skill. Natural extension when dashboards are comprehensive. Prerequisite for fc-014 and fc-015."}]} +{"version":"1.0.0","description":"Deferred features and decisions for future implementation","items":[{"id":"fc-001","category":"infrastructure","title":"Workstation monitoring with Prometheus","description":"Deploy node_exporter and Alertmanager for workstation metrics and alerting","priority":"medium","status":"deferred","created":"2024-12-28","notes":"Would enable proactive alerting for disk, memory, CPU issues"},{"id":"fc-002","category":"agent","title":"Network admin agent","description":"Agent for network configuration, firewall rules, VPN management","priority":"medium","status":"deferred","created":"2024-12-28","notes":"Would manage iptables/nftables, NetworkManager, WireGuard"},{"id":"fc-003","category":"agent","title":"Personal assistant agent","description":"Agent for personal tasks, reminders, scheduling","priority":"medium","status":"deferred","created":"2024-12-28","notes":"Integration with calendar, task management"},{"id":"fc-004","category":"integration","title":"External LLM integration","description":"Support for non-Claude models in the agent system","priority":"low","status":"deferred","created":"2024-12-28","notes":"For specialized tasks or cost optimization"},{"id":"fc-005","category":"optimization","title":"Model usage logging and cost tracking","description":"Track model usage across agents for cost analysis","priority":"medium","status":"deferred","created":"2024-12-28","notes":"Would help optimize model selection policy"},{"id":"fc-006","category":"design","title":"Slash commands redesign","description":"Revisit slash command architecture and user experience","priority":"low","status":"deferred","created":"2024-12-28","notes":"Current design may need refinement"},{"id":"fc-007","category":"optimization","title":"Document structure optimization","description":"Optimize agent document format for efficiency","priority":"low","status":"deferred","created":"2024-12-28","notes":"Balance between clarity and token usage"},{"id":"fc-008","category":"infrastructure","title":"ArgoCD CLI authentication","description":"Configure argocd CLI with proper authentication","priority":"medium","status":"resolved","created":"2025-12-28","resolved":"2025-12-28","notes":"Using 10-year API token (expires 2035-12-26). Token ID: e3980c6a-1c4e-4f1a-8459-a120a5c60cc5. Stored in ~/.config/argocd/config. No renewal automation needed."},{"id":"fc-009","category":"infrastructure","title":"Prometheus local port-forward","description":"Document Prometheus access patterns for agents","priority":"low","status":"identified","created":"2025-12-28","notes":"Prometheus not accessible on localhost:9090. Options: (1) use kubectl exec to query, (2) set up port-forward, (3) use ingress. Currently works via pod exec."},{"id":"fc-010","category":"infrastructure","title":"Clone homelab gitops repo locally","description":"Clone git@github.com:will666/homelab.git for git-operator access","priority":"low","status":"resolved","created":"2025-12-28","resolved":"2025-12-28","notes":"Cloned to ~/.claude/repos/homelab"},{"id":"fc-011","category":"k8s-health","title":"Address OutOfSync ArgoCD apps","description":"5 apps OutOfSync, 1 Degraded (porthole)","priority":"medium","status":"identified","created":"2025-12-28","notes":"OutOfSync: adopt-a-street, ai-stack, gitea, home-assistant, kubernetes-dashboard, speetest-tracker. Degraded: porthole"},{"id":"fc-012","category":"agent-memory","title":"PA knowledge base with session caching","description":"Local KB for infrastructure facts with lazy-load and in-session caching","priority":"medium","status":"resolved","created":"2025-12-28","resolved":"2025-12-28","notes":"Implemented. KB files at state/kb.json (shared) and state/personal-assistant/kb.json (private). PA agent updated with lazy-load behavior."},{"id":"fc-013","category":"agent-memory","title":"Vector database for agent long-term memory","description":"Semantic search over agent knowledge using embeddings","priority":"low","status":"deferred","created":"2025-12-28","notes":"Would enable fuzzy matching, semantic queries, and scalable knowledge storage. Consider: ChromaDB, Qdrant, or pgvector."},{"id":"fc-014","category":"observability","title":"Grafana predefined reports","description":"Slash command like /grafana-report services to get standard metrics from known dashboards","priority":"low","status":"deferred","created":"2025-12-29","notes":"Requires comprehensive dashboard coverage first. Revisit when observability matures."},{"id":"fc-015","category":"observability","title":"Grafana integration in diagnostics","description":"Auto-pull Grafana dashboard data during /k8s:diagnose or health checks","priority":"low","status":"deferred","created":"2025-12-29","notes":"Would make Grafana the first troubleshooting tool. Depends on fc-016 and mature observability setup."},{"id":"fc-016","category":"observability","title":"Extend prometheus-analyst with Grafana API","description":"Add Grafana API query capability to existing prometheus-analyst agent","priority":"low","status":"deferred","created":"2025-12-29","notes":"Preferred approach over creating new agent/skill. Natural extension when dashboards are comprehensive. Prerequisite for fc-014 and fc-015."},{"id":"fc-017","category":"optimization","title":"Subagent spawning for skill delegation","description":"Implement Task tool or similar mechanism to spawn lower-tier models for specific operations","priority":"medium","status":"resolved","created":"2025-12-31","resolved":"2025-12-31","notes":"Implemented via Claude CLI subprocess. Helper at ~/.claude/mcp/delegation/gmail_delegate.py. Uses tiered delegation: fetch/list (no LLM), Sonnet for summarize/triage (via 'claude --print --model sonnet'). Uses subscription, no API key needed."}]} diff --git a/state/model-policy.json b/state/model-policy.json index c6014a6..bfb5e3b 100644 --- a/state/model-policy.json +++ b/state/model-policy.json @@ -64,5 +64,54 @@ "default_start": "lowest_capable", "log_usage": true, "review_frequency": "weekly" + }, + "skill_delegation": { + "gmail": { + "description": "Tiered model selection for email operations", + "tiers": { + "haiku": { + "operations": [ + "count_unread", + "list_emails", + "fetch_metadata", + "simple_search" + ], + "examples": [ + "How many unread emails?", + "List emails from sender X", + "Any emails with attachments?" + ] + }, + "sonnet": { + "operations": [ + "summarize_email", + "summarize_thread", + "categorize_emails", + "extract_action_items", + "group_by_topic" + ], + "examples": [ + "Summarize this email", + "What are the OpenAgents notifications about?", + "Group my emails by project" + ] + }, + "opus": { + "operations": [ + "prioritize_inbox", + "strategic_analysis", + "cross_reference_context", + "complex_reasoning" + ], + "examples": [ + "What should I respond to first?", + "How does this relate to my current projects?", + "What's the sentiment across these threads?" + ] + } + }, + "default": "haiku", + "escalate_on": ["insufficient_context", "reasoning_required", "user_dissatisfied"] + } } }