Install
openclaw skills install ocas-tasteGenerates personalized recommendations from real consumption data by scanning email/calendar, enriching venues, and explaining suggestions with prior behavior.
openclaw skills install ocas-tasteTaste builds a personalized taste model from real consumption signals — purchases, restaurant visits, food delivery orders, hotel stays, music plays, and movie watches. It scans the user's email and calendar to automatically extract these signals, enriches venue entities with taste-relevant attributes (cuisine, price point, neighborhood, vibe) via Google Maps and web search, and uses temporal decay so recent behavior outweighs stale history. Every recommendation names the specific prior consumption that justifies it, respects dietary restrictions, and only suggests places the user hasn't been.
Taste owns behavior-driven preference modeling, consumption signal extraction from email/calendar, entity enrichment for taste profiling, and evidence-backed recommendations.
Taste does not own: web research (Sift), social graph (Weave), knowledge graph (Elephas), pattern analysis (Corvus), browsing interpretation (Thread).
taste.scan — scan the user's email and calendar for consumption signals; extract, deduplicate, and promote to signals; queue new items for enrichmenttaste.scan.report — summarize last scan: extractions processed, signals created, cancellations, dedup matches pending reviewtaste.ingest.signal — manually record a consumption signal (purchase, visit, play, watch, stay)taste.enrich.item — enrich an item with taste-relevant attributes via Google Maps lookup and web searchtaste.query.recommend — generate recommendations grounded in consumption history, enriched attributes, and frequency patterns; respects dietary restrictions; only suggests new placestaste.query.serendipity — find novel but defensible cross-domain connectionstaste.model.status — return model state: signal count, domains active, enrichment coverage, stalenesstaste.report.weekly — generate a weekly taste pattern summarytaste.journal — write journal for the current run; called at end of every runtaste.update — pull latest from GitHub source; preserves journals and datataste.scan)references/email_extraction.md for sender allowlist)references/email_extraction.md)taste.enrich.item)enriched: false, look up the venue/item on Google Mapsreferences/enrichment.md)enriched: true and enriched_attaste.ingest.signal)taste.query.recommend)references/signal_policy.md)references/strength_model.md)config.json → user_preferences)Signal strength and recency both matter. See references/strength_model.md for full model. Key points:
decay.halflife_days (default 180)~/openclaw/data/ocas-taste/
config.json
signals.jsonl
items.jsonl
links.jsonl
decisions.jsonl
extractions.jsonl
reports/
~/openclaw/journals/ocas-taste/
YYYY-MM-DD/
{run_id}.json
Default config.json:
{
"skill_id": "ocas-taste",
"skill_version": "3.0.0",
"config_version": "2",
"created_at": "",
"updated_at": "",
"domains": {
"enabled": ["music", "restaurant", "book", "movie", "product", "travel", "event"]
},
"decay": {
"halflife_days": 180
},
"retention": {
"days": 0,
"max_records": 10000
},
"email_scan": {
"enabled": true,
"last_scan_timestamp": null,
"extraction_confidence_threshold": 0.6,
"auto_promote_threshold": 0.8
},
"email_sources": {
"doordash": { "sender_patterns": ["no-reply@doordash.com", "orders@doordash.com"], "domain": "restaurant", "source_type": "purchase" },
"instacart": { "sender_patterns": ["no-reply@instacart.com"], "domain": "product", "source_type": "purchase" },
"good_eggs": { "sender_patterns": ["*@goodeggs.com"], "domain": "product", "source_type": "purchase" },
"tock": { "sender_patterns": ["*@exploretock.com"], "domain": "restaurant", "source_type": "visit" },
"opentable": { "sender_patterns": ["*@opentable.com"], "domain": "restaurant", "source_type": "visit" },
"yelp": { "sender_patterns": ["no-reply@yelp.com"], "domain": "restaurant", "source_type": "visit" },
"amazon": { "sender_patterns": ["auto-confirm@amazon.com", "ship-confirm@amazon.com"], "domain": "product", "source_type": "purchase" },
"hotels": { "sender_patterns": ["*@booking.com", "*@hotels.com", "*@marriott.com", "*@hilton.com", "*@hyatt.com", "*@ihg.com", "*@airbnb.com"], "domain": "travel", "source_type": "stay" }
},
"strength": {
"base_purchase": 0.80,
"base_visit": 0.70,
"base_stay": 0.75,
"base_play": 0.60,
"base_watch": 0.60,
"base_manual": 0.60,
"frequency_bonus_per_visit": 0.05,
"frequency_bonus_cap": 0.15,
"recency_bonus_days": 30,
"recency_bonus_value": 0.05
},
"user_preferences": {
"dietary_restrictions": [],
"dietary_preferences": [],
"cuisine_dislikes": [],
"notes": ""
}
}
Universal OKRs from spec-ocas-journal.md apply to all runs.
skill_okrs:
- name: recommendation_evidence_rate
metric: fraction of recommendations citing at least one consumed item
direction: maximize
target: 1.0
evaluation_window: 30_runs
- name: serendipity_novelty
metric: fraction of serendipity results crossing domain boundaries
direction: maximize
target: 0.80
evaluation_window: 30_runs
- name: signal_freshness
metric: fraction of active signals within decay half-life
direction: maximize
target: 0.60
evaluation_window: 30_runs
- name: email_extraction_coverage
metric: fraction of transactional emails successfully extracted with confidence >= threshold
direction: maximize
target: 0.90
evaluation_window: 30_runs
- name: dedup_accuracy
metric: fraction of dedup groupings not subsequently corrected by manual review
direction: maximize
target: 0.95
evaluation_window: 30_runs
- name: enrichment_coverage
metric: fraction of items with enriched = true
direction: maximize
target: 0.90
evaluation_window: 30_runs
Observation Journal — all signal ingestion, scan, enrichment, query, and report runs.
On first invocation of any Taste command, run taste.init:
~/openclaw/data/ocas-taste/ and subdirectories (reports/)config.json with all fields if absentsignals.jsonl, items.jsonl, links.jsonl, decisions.jsonl, extractions.jsonl~/openclaw/journals/ocas-taste/taste:update if not already present (check openclaw cron list first)decisions.jsonl| Job name | Mechanism | Schedule | Command |
|---|---|---|---|
taste:update | cron | 0 0 * * * (midnight daily) | taste.update |
openclaw cron add --name taste:update --schedule "0 0 * * *" --command "taste.update" --sessionTarget isolated --lightContext true --timezone America/Los_Angeles
taste.update pulls the latest package from the source: URL in this file's frontmatter. Runs silently — no output unless the version changed or an error occurred.
source: from frontmatter → extract {owner}/{repo} from URLskill.jsongh api "repos/{owner}/{repo}/contents/skill.json" --jq '.content' | base64 -d | python3 -c "import sys,json;print(json.load(sys.stdin)['version'])"TMPDIR=$(mktemp -d)
gh api "repos/{owner}/{repo}/tarball/main" > "$TMPDIR/archive.tar.gz"
mkdir "$TMPDIR/extracted"
tar xzf "$TMPDIR/archive.tar.gz" -C "$TMPDIR/extracted" --strip-components=1
cp -R "$TMPDIR/extracted/"* ./
rm -rf "$TMPDIR"
I updated Taste from version {old} to {new}public
| File | When to read |
|---|---|
references/schemas.md | Before creating signals, items, links, extractions, or recommendations |
references/signal_policy.md | Before decay calculations or domain gating |
references/strength_model.md | Before computing signal strength or ranking items |
references/email_extraction.md | Before running taste.scan; sender allowlist and dedup rules |
references/enrichment.md | Before running taste.enrich.item; what to look up and extract per domain |
references/recommendation_style.md | Before generating recommendations or reports |
references/journal.md | Before taste.journal; at end of every run |