Taste
by @indigokarasu
Generates personalized recommendations from real consumption data by scanning email/calendar, enriching venues, and explaining suggestions with prior behavior.
clawhub install ocas-tasteπ About This Skill
name: ocas-taste source: https://github.com/indigokarasu/taste install: openclaw skill install https://github.com/indigokarasu/taste description: Use when generating personalized recommendations grounded in real consumption signals (purchases, visits, plays, watches), scanning email and calendar for consumption data, enriching venue entities with taste-relevant attributes, exploring cross-domain discovery based on actual behavior, checking taste model status, or producing periodic taste pattern reports. Trigger phrases: 'recommend', 'what would I like', 'based on what I've liked', 'suggest something similar', 'my taste', 'what should I try', 'scan my email', 'what have I been eating', 'restaurant recommendations', 'update taste'. Do not use for generic search, editorial top-10 lists, or ad-copy generation. metadata: {"openclaw":{"emoji":"π―"}}
Taste
Taste 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.
When to use
Do not use
Responsibility boundary
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).
Commands
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 dataOperating invariants
Workflows
Email/calendar scan workflow (taste.scan)
1. Access the user's email and search for transactional messages from known services (see references/email_extraction.md for sender allowlist)
2. Access the user's Google Calendar for restaurant reservations and hotel bookings
3. For each matching message/event, extract structured data into an ExtractionRecord
4. Classify email_type: confirmation, reminder, update, cancellation, receipt
5. Compute dedup_key and run dedup pass (see references/email_extraction.md)
6. Exclude cancelled events from promotion
7. Promote valid, non-duplicate extractions to ConsumptionSignals
8. Create or update ItemRecords (increment signal_count, append to visit_dates)
9. Queue unenriched items for enrichment
10. Persist all records
11. Write journal
Enrichment workflow (taste.enrich.item)
1. For items with enriched: false, look up the venue/item on Google Maps
2. Extract taste-relevant attributes: cuisine, price level, neighborhood, vibe, rating (see references/enrichment.md)
3. If Google Maps data is insufficient, use web search to fill gaps
4. Update ItemRecord metadata with enriched attributes
5. Set enriched: true and enriched_at
6. Evaluate and create LinkRecords between items sharing attributes
7. Persist updates
Signal ingestion workflow (taste.ingest.signal)
1. Receive or normalize input signal 2. Validate domain and signal structure 3. Persist signal 4. Create or update ItemRecord 5. Queue for enrichment if new item 6. Write journal
Recommendation workflow (taste.query.recommend)
1. Load all active signals, apply temporal decay (see references/signal_policy.md)
2. Compute effective item strength with frequency and recency bonuses (see references/strength_model.md)
3. Rank items by effective strength within each domain
4. Identify taste patterns from enriched attributes (cuisine clusters, price preferences, neighborhood tendencies)
5. Generate recommendations for *new* venues that match identified patterns
6. Verify each recommendation against dietary restrictions (config.json β user_preferences)
7. Verify each recommendation is not a place the user has visited (check signals/items)
8. Include evidence-linked explanation citing specific consumed items and frequency
9. Write journal
Signal weighting and decay
Signal strength and recency both matter. See references/strength_model.md for full model. Key points:
decay.halflife_days (default 180)Storage layout
~/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": ""
}
}
OKRs
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
Skill cooperation
Journal outputs
Observation Journal β all signal ingestion, scan, enrichment, query, and report runs.
Initialization
On first invocation of any Taste command, run taste.init:
1. Create ~/openclaw/data/ocas-taste/ and subdirectories (reports/)
2. Write default config.json with all fields if absent
3. Create empty JSONL files: signals.jsonl, items.jsonl, links.jsonl, decisions.jsonl, extractions.jsonl
4. Create ~/openclaw/journals/ocas-taste/
5. Register cron job taste:update if not already present (check openclaw cron list first)
6. Log initialization as a DecisionRecord in decisions.jsonl
Background tasks
| 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
Self-update
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.
1. Read source: from frontmatter β extract {owner}/{repo} from URL
2. Read local version from skill.json
3. Fetch remote version: gh api "repos/{owner}/{repo}/contents/skill.json" --jq '.content' | base64 -d | python3 -c "import sys,json;print(json.load(sys.stdin)['version'])"
4. If remote version equals local version β stop silently
5. Download and install:
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"
6. On failure β retry once. If second attempt fails, report the error and stop.
7. Output exactly: I updated Taste from version {old} to {new}Visibility
public
Support file map
| 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 |