Add email-to-expense skill; expand log_expense.py to accept other/professional-development/software types; update skill index
This commit is contained in:
parent
e7b3084622
commit
20fc0f8d0b
3 changed files with 121 additions and 2 deletions
|
|
@ -41,6 +41,7 @@ description: Master index of all skills in your robot assistant system. Your ass
|
||||||
| "add this to my brag sheet," "log this kudos," "save this feedback," "add to brag sheet," "log a win" | **brag-sheet** |
|
| "add this to my brag sheet," "log this kudos," "save this feedback," "add to brag sheet," "log a win" | **brag-sheet** |
|
||||||
| "support punt," "forward to support," "send to support," "punt this to support," "hand off to support" | **send-to-support** |
|
| "support punt," "forward to support," "send to support," "punt this to support," "hand off to support" | **send-to-support** |
|
||||||
| "add a contact," "look up a contact," "find someone's number," "update a contact," "delete a contact," "list my contacts," "contacts" | **contacts** |
|
| "add a contact," "look up a contact," "find someone's number," "update a contact," "delete a contact," "list my contacts," "contacts" | **contacts** |
|
||||||
|
| "process my expense emails," "email to expense," "triage my expense label," "process expense label" | **email-to-expense** |
|
||||||
| "transcribe this video," "get the subtitles," "what does this video say," "summarize this YouTube video," "YouTube transcript" | **youtube-transcript** |
|
| "transcribe this video," "get the subtitles," "what does this video say," "summarize this YouTube video," "YouTube transcript" | **youtube-transcript** |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
@ -215,6 +216,12 @@ description: Master index of all skills in your robot assistant system. Your ass
|
||||||
**File:** `skills/contacts/SKILL.md`
|
**File:** `skills/contacts/SKILL.md`
|
||||||
**Dependencies:** `uv` CLI, Python 3.12+, `contacts` script at `skills/contacts/scripts/contacts`
|
**Dependencies:** `uv` CLI, Python 3.12+, `contacts` script at `skills/contacts/scripts/contacts`
|
||||||
|
|
||||||
|
### Email to Expense
|
||||||
|
**Purpose:** Process emails from a Gmail label into work expenses. Opens Gmail via browser automation, iterates through emails in the "expense" label, saves each as PDF, extracts metadata (date, sender, subject), uploads to S2, classifies the expense type, and inserts into MongoDB via log_expense.py.
|
||||||
|
**Triggers:** "process my expense emails," "email to expense," "triage my expense label," "process expense label"
|
||||||
|
**File:** `skills/email-to-expense/SKILL.md`
|
||||||
|
**Dependencies:** Browser tool, user's Gmail logged in, S2 upload endpoint, log-work-expense skill scripts
|
||||||
|
|
||||||
### YouTube Transcript
|
### YouTube Transcript
|
||||||
**Purpose:** Download and summarize transcripts from YouTube videos using yt-dlp. Cleans VTT subtitles into readable plain text and summarizes or analyzes content as requested.
|
**Purpose:** Download and summarize transcripts from YouTube videos using yt-dlp. Cleans VTT subtitles into readable plain text and summarizes or analyzes content as requested.
|
||||||
**Triggers:** "transcribe this video," "get the subtitles," "what does this video say," "summarize this YouTube video," "YouTube transcript"
|
**Triggers:** "transcribe this video," "get the subtitles," "what does this video say," "summarize this YouTube video," "YouTube transcript"
|
||||||
|
|
|
||||||
106
email-to-expense/SKILL.md
Normal file
106
email-to-expense/SKILL.md
Normal file
|
|
@ -0,0 +1,106 @@
|
||||||
|
---
|
||||||
|
name: email-to-expense
|
||||||
|
description: |
|
||||||
|
Process emails from a Gmail label into work expenses. Opens Gmail via browser automation,
|
||||||
|
iterates through emails in the "expense" label, saves each as PDF, extracts metadata,
|
||||||
|
and logs them to MongoDB via the log-work-expense skill.
|
||||||
|
|
||||||
|
Triggers when user mentions:
|
||||||
|
- "process my expense emails"
|
||||||
|
- "email to expense"
|
||||||
|
- "triage my expense label"
|
||||||
|
- "process expense label"
|
||||||
|
---
|
||||||
|
|
||||||
|
# Email to Expense
|
||||||
|
|
||||||
|
Pulls emails from Gmail's "expense" label, saves them as PDFs, extracts metadata, and inserts them into `wip.work_expenses`.
|
||||||
|
|
||||||
|
## Prerequisites
|
||||||
|
|
||||||
|
- Browser tool available (`openclaw browser`)
|
||||||
|
- User's Gmail must be logged in (use `profile="user"`)
|
||||||
|
- `log-work-expense` skill scripts accessible at `~/notes/skills/log-work-expense/scripts/`
|
||||||
|
|
||||||
|
## Flow
|
||||||
|
|
||||||
|
### 1. Open Gmail with expense label
|
||||||
|
|
||||||
|
Open Gmail filtered to the "expense" label using the user's browser profile:
|
||||||
|
|
||||||
|
```
|
||||||
|
URL: https://mail.google.com/mail/u/0/#label/expense
|
||||||
|
```
|
||||||
|
|
||||||
|
If the label name has spaces or special characters, URL-encode it. The label may also be accessed as "2 expense" — adjust accordingly.
|
||||||
|
|
||||||
|
Take a snapshot to see how many emails are in the list. Note the email count.
|
||||||
|
|
||||||
|
### 2. Iterate through each email
|
||||||
|
|
||||||
|
For each email in the label (oldest first to maintain order):
|
||||||
|
|
||||||
|
1. **Click the email** to open it
|
||||||
|
2. **Snapshot the open email** to extract metadata:
|
||||||
|
- **Date** — from the email header (parse the displayed date string)
|
||||||
|
- **Sender** — from the "From" field
|
||||||
|
- **Subject** — from the subject line
|
||||||
|
3. **Save as PDF** — Use the browser's print-to-PDF functionality:
|
||||||
|
- Use `action="pdf"` if available, or trigger `Ctrl+P` and save to a temp directory
|
||||||
|
- Save to `/tmp/email_expense_<index>.pdf`
|
||||||
|
4. **Upload PDF to S2** — Use the S2 upload endpoint from TOOLS.md:
|
||||||
|
```bash
|
||||||
|
curl -X POST https://api.connorrhodes.com/agent/s2_upload \
|
||||||
|
-H "x-api-key: LT6CXiLT5cEApfqtThz17bENr6OLp804FepOMqa1tZkfTGXiiCcSFlupl6gaYeX" \
|
||||||
|
-F "file=@/tmp/email_expense_<index>.pdf"
|
||||||
|
```
|
||||||
|
Collect the returned S2 URL.
|
||||||
|
5. **Return to the email list** — Go back to the label view
|
||||||
|
|
||||||
|
### 3. Classify each expense
|
||||||
|
|
||||||
|
After collecting all emails, classify each one:
|
||||||
|
|
||||||
|
**Known merchants (auto-classify):**
|
||||||
|
|
||||||
|
| Sender / Keyword | Type | Default Account |
|
||||||
|
|-------------------|------|-----------------|
|
||||||
|
| DoorDash | meal | _(ask user)_ |
|
||||||
|
| Uber Eats | meal | _(ask user)_ |
|
||||||
|
| Amazon | other | _(ask user)_ |
|
||||||
|
|
||||||
|
For unknown senders, **ask the user** to classify:
|
||||||
|
- Type: `meal`, `mileage`, `other`, `professional-development`, `software`, etc.
|
||||||
|
- Account: the work account to bill against
|
||||||
|
|
||||||
|
### 4. Insert into MongoDB
|
||||||
|
|
||||||
|
For each classified email, run the existing log_expense.py script:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
uv run --with pymongo ~/notes/skills/log-work-expense/scripts/log_expense.py \
|
||||||
|
<type> <account> <date> "<note>" <s2_url>
|
||||||
|
```
|
||||||
|
|
||||||
|
- **type**: meal, mileage, or other
|
||||||
|
- **account**: from classification step
|
||||||
|
- **date**: parsed from the email header (YYYY-MM-DD format)
|
||||||
|
- **note**: include sender and subject for context, e.g. "DoorDash - order confirmation"
|
||||||
|
- **s2_url**: the uploaded PDF URL
|
||||||
|
|
||||||
|
### 5. Confirm
|
||||||
|
|
||||||
|
Show a summary table of all processed expenses:
|
||||||
|
- Sender | Date | Type | Account | Status
|
||||||
|
|
||||||
|
### 6. Cleanup
|
||||||
|
|
||||||
|
- Remove local PDF files from `/tmp/`
|
||||||
|
- Optionally archive/remove the processed label from Gmail (ask user first)
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
|
||||||
|
- Always ask the user to confirm before inserting expenses into the database
|
||||||
|
- If an email doesn't look like an expense (no receipt, no order confirmation, etc.), skip it and note it
|
||||||
|
- The Gmail label URL format: `https://mail.google.com/mail/u/0/#label/<labelname>`
|
||||||
|
- For labels with spaces, the URL typically uses the raw label name
|
||||||
|
|
@ -95,8 +95,9 @@ def main():
|
||||||
note = args[3]
|
note = args[3]
|
||||||
files = args[4:]
|
files = args[4:]
|
||||||
|
|
||||||
if exp_type not in ("meal", "mileage"):
|
valid_types = ("meal", "mileage", "other", "professional-development", "software")
|
||||||
print(f"Error: type must be 'meal' or 'mileage', got '{exp_type}'")
|
if exp_type not in valid_types:
|
||||||
|
print(f"Error: type must be one of {valid_types}, got '{exp_type}'")
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
if exp_type == "meal" and len(files) != 1:
|
if exp_type == "meal" and len(files) != 1:
|
||||||
|
|
@ -107,6 +108,11 @@ def main():
|
||||||
print(f"Error: mileage entries must have exactly 2 files (start + end odometer), got {len(files)}")
|
print(f"Error: mileage entries must have exactly 2 files (start + end odometer), got {len(files)}")
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
|
# Non-mileage/non-meal types require exactly 1 file
|
||||||
|
if exp_type not in ("meal", "mileage") and len(files) != 1:
|
||||||
|
print(f"Error: {exp_type} entries must have exactly 1 file, got {len(files)}")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
if route_stops is not None and exp_type != "mileage":
|
if route_stops is not None and exp_type != "mileage":
|
||||||
print("Error: --route is only valid for mileage entries")
|
print("Error: --route is only valid for mileage entries")
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue