Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
107 changes: 107 additions & 0 deletions community/life-admin-tracker/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
# Life Admin & Renewals Tracker

Never miss a renewal again. Track passports, MOTs, insurance, warranties, subscriptions — anything with an expiry date. Log once by voice, get proactive reminders automatically. No apps, no calendar entries, no forgetting.

## Trigger Phrases

- `"life admin"` / `"my renewals"` / `"renewals tracker"`
- `"what's due soon"` / `"what's expiring"` / `"anything coming up"`
- `"add a renewal"` / `"track my passport"` / `"track my MOT"`
- `"due soon"` / `"expiring soon"`
- `"mark as renewed"` / `"subscription costs"`

## What It Tracks

| Category | Examples |
|---|---|
| Document | Passport, driving licence, ID card, work permit |
| Vehicle | MOT, car insurance, road tax, service interval |
| Home | Boiler service, lease renewal, contents insurance |
| Subscription | Netflix, Spotify, gym, software — with optional cost |
| Warranty | Phone, laptop, washing machine |
| Other | Anything else with an expiry date |

## How It Works

**First time:** Quick setup — just your name.

**Adding an item (3 exchanges, never more):**
> User: "Add a renewal"
> AI: "What would you like to track?"
> User: "My passport"
> AI: "When does it expire?"
> User: "March 2027"
> AI: "Got it — Passport logged, expires in about 7 months. I'll remind you 90, 30, and 7 days before."

Natural date input always works: "next March", "in 3 years", "2027-03-15", "March 2027", "sometime next year".

If you mention the item AND date in your first sentence ("track my passport, expires March 2027"), setup is a single exchange.

**Checking what's due:**
> User: "What's due soon?"
> AI: "Your MOT in 8 days, your car insurance in 22 days, and your passport in about 4 months. Plus 2 more items further out."

Items sorted by urgency — most critical always spoken first.

**Renewing an item:**
> User: "Mark my MOT as renewed"
> AI: "When is the new expiry date?"
> User: "Year from now"
> AI: "Done — MOT updated. Next renewal in about a year."

Renewal chaining: the ability automatically asks for the next date and continues the tracking cycle.

**Checking subscription costs:**
> User: "Subscription costs"
> AI: "You have 4 subscriptions: Netflix £11 a month, Spotify £10 a month, gym £35 a month, Adobe £55 a month. That's £111 a month or £1,332 a year."

**Checking one item:**
> User: "What's the status on my passport?"
> AI: "Your Passport expires on 2027-03-15 — that's in about 7 months."

## Background Daemon — Automatic Reminders

Runs hourly. No action needed from you.

| Days to expiry | What happens |
|---|---|
| 90 days | First heads-up (fires once) |
| 30 days | Second alert (fires once) |
| ≤ 7 days | Daily urgent alert until renewed |
| Expired | Daily reminder until renewed |

Multiple urgencies are grouped into one message:
> "Urgent — 2 things need attention: your MOT tomorrow and your car insurance in 5 days."

## What Makes It Different from Alexa

| | Alexa | Life Admin Tracker |
|---|---|---|
| Remembers renewals across sessions | No | Yes — permanently |
| Proactive expiry alerts | No | Yes — automatic |
| Natural date input ("next March") | No | Yes — LLM extraction |
| Grouped urgency alerts | No | Yes |
| Subscription cost intelligence | No | Yes |
| Renewal chaining (auto-asks next date) | No | Yes |
| Multiple urgencies in one message | No | Yes |

## Setup

No API keys required. Runs entirely on the built-in LLM and persistent storage.

## Example Sessions

**Morning, 6 items tracked:**
> Background daemon: "Heads up — your MOT expires in 28 days and your home insurance in 3 months."

**Adding a subscription:**
> User: "Track my Spotify"
> AI: "When does it renew?"
> User: "Every month, next renewal is September"
> AI: "How much does Spotify cost? Say 'skip' to leave that out."
> User: "£10 a month"
> AI: "Got it — Spotify logged at £10/month, renewing in about a month. I'll remind you 90, 30, and 7 days before."

**Removing something:**
> User: "Remove Netflix"
> AI: "Removed Netflix."
Empty file.
167 changes: 167 additions & 0 deletions community/life-admin-tracker/background.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,167 @@
from datetime import datetime

from src.agent.capability import MatchingCapability
from src.agent.capability_worker import CapabilityWorker
from src.main import AgentWorker

STORAGE_KEY = "life_admin_tracker_data"

# Days-before-expiry thresholds, ascending so we find the tightest match first
NUDGE_THRESHOLDS = [0, 7, 30, 90]


class LifeAdminTrackerBackgroundCapability(MatchingCapability):
worker: AgentWorker = None
capability_worker: CapabilityWorker = None
background_daemon_mode: bool = False

# Do not change following tag of register capability
# {{register capability}}

def call(self, worker: AgentWorker, background_daemon_mode: bool):
self.background_daemon_mode = background_daemon_mode
self.worker = worker
self.capability_worker = CapabilityWorker(self.worker)
self.worker.session_tasks.create(self.watch_loop())

# ------------------------------------------------------------------
# Storage
# ------------------------------------------------------------------

def _load_data(self) -> dict:
try:
result = self.capability_worker.get_single_key(STORAGE_KEY)
if result and result.get("value"):
return result["value"]
except Exception as e:
self.worker.editor_logging_handler.error(f"[LifeAdminBG] Load error: {e!r}")
return {}

def _save_data(self, data: dict):
try:
result = self.capability_worker.create_key(STORAGE_KEY, data)
if not result.get("success"):
self.capability_worker.update_key(STORAGE_KEY, data)
except Exception as e:
self.worker.editor_logging_handler.error(f"[LifeAdminBG] Save error: {e!r}")

# ------------------------------------------------------------------
# Helpers
# ------------------------------------------------------------------

def _days_until(self, expiry_date: str) -> int:
try:
exp = datetime.strptime(expiry_date, "%Y-%m-%d").date()
return (exp - datetime.now().date()).days
except Exception:
return 9999

def _today(self) -> str:
return datetime.now().strftime("%Y-%m-%d")

def _nudge_threshold_for(self, days: int) -> int:
"""Return the tightest matching nudge threshold, or -1 if >90 days out."""
for threshold in NUDGE_THRESHOLDS:
if days <= threshold:
return threshold
return -1

def _format_days(self, days: int) -> str:
if days <= 0:
return "already expired"
if days == 1:
return "tomorrow"
return f"in {days} days"

# ------------------------------------------------------------------
# Daemon loop
# ------------------------------------------------------------------

async def watch_loop(self):
self.capability_worker.resume_normal_flow()
self.worker.editor_logging_handler.info("[LifeAdminBG] Daemon started")

while True:
try:
await self.worker.session_tasks.sleep(3600.0)

data = self._load_data()
if not data.get("setup_complete"):
continue

items = data.get("items", [])
if not items:
continue

name = data.get("user_name", "")
today = self._today()
nudge_items = []
data_changed = False

for item in items:
days = self._days_until(item.get("expiry_date", ""))
threshold = self._nudge_threshold_for(days)
if threshold == -1:
continue

last_threshold = item.get("last_nudge_threshold")
last_date = item.get("last_nudge_date", "")

if threshold <= 7:
# Daily nudges inside the 7-day window
if last_date != today:
nudge_items.append((item, days))
item["last_nudge_threshold"] = threshold
item["last_nudge_date"] = today
data_changed = True
else:
# One-time nudge per threshold (90-day, 30-day)
if last_threshold != threshold:
nudge_items.append((item, days))
item["last_nudge_threshold"] = threshold
data_changed = True

if data_changed:
self._save_data(data)

if not nudge_items:
continue

name_str = f", {name}" if name else ""
is_urgent = any(d <= 7 for _, d in nudge_items)
prefix = f"Urgent{name_str}" if is_urgent else f"Heads up{name_str}"

if len(nudge_items) == 1:
item, days = nudge_items[0]
if days <= 0:
msg = (
f"{prefix} — your {item['name']} has expired. "
f"Time to get it renewed."
)
else:
msg = (
f"{prefix} — your {item['name']} expires "
f"{self._format_days(days)}."
)
else:
parts = []
for item, days in nudge_items[:3]:
parts.append(f"your {item['name']} {self._format_days(days)}")

if len(parts) == 2:
items_str = f"{parts[0]} and {parts[1]}"
else:
items_str = (
", ".join(parts[:-1]) + f", and {parts[-1]}"
)
msg = f"{prefix} — {len(nudge_items)} things need attention: {items_str}."

self.worker.editor_logging_handler.info(
f"[LifeAdminBG] Nudging: {[i['name'] for i, _ in nudge_items]}"
)
await self.capability_worker.send_interrupt_signal()
await self.capability_worker.speak(msg)

except Exception as e:
self.worker.editor_logging_handler.error(f"[LifeAdminBG] Loop error: {e!r}")
await self.worker.session_tasks.sleep(300.0)
Loading
Loading