semantic-context-matching
Match real-time context signals (GPS, weather, time, behavior) to approved offers for optimal activation moment. Scores offers 0-100 and activates if >60.
SKILL.md.claude/skills/semantic-context-matching/SKILL.md
GitHubSemantic Context Matching Skill
Match real-time context signals to approved offers in The Hub. Determines optimal activation moment based on location, time, weather, and behavioral alignment.
Trigger Phrases
Auto-invokes when detecting:
- "match context to offer"
- "context matching"
- "semantic match"
- "score offers"
- "find relevant offer"
- When Scout receives context update signal
Execution Steps
Step 1: Receive Context Signal
Input format:
{
"member_id": "member-12345",
"timestamp": "2026-03-26T14:30:00Z",
"location": {
"lat": 43.6532,
"lon": -79.3832,
"accuracy_meters": 50
},
"time": {
"hour": 14,
"day_of_week": "wednesday",
"is_weekend": false
},
"weather": {
"temperature_celsius": -5,
"conditions": "cold",
"weather_code": "clear_sky"
},
"behavior": {
"last_purchase_date": "2026-02-15",
"last_purchase_category": "outdoor",
"days_since_visit": 39,
"recent_categories": ["outdoor", "automotive"],
"visit_frequency_per_month": 2.5
}
}
Step 2: Fetch Approved Offers from Hub
Query Hub API for offers with status: "active" and eligible for this member's segment.
active_offers = await hub.get_offers(
status="active",
segment=member.segment,
valid_for_member=member_id
)
Filter criteria:
- Status must be "active"
- Offer not yet delivered to this member (check audit log)
- Offer within frequency cap (respects 24h rule)
- Segment criteria match member profile
Step 3: Score Each Offer
Scoring algorithm: 4 dimensions, total 100 points
Dimension 1: Location Match (40 points max)
Calculate distance between member's GPS coordinates and nearest relevant store:
def calculate_distance(lat1, lon1, lat2, lon2) -> float:
# Haversine formula
R = 6371 # Earth radius in km
dlat = radians(lat2 - lat1)
dlon = radians(lon2 - lon1)
a = sin(dlat/2)**2 + cos(radians(lat1)) * cos(radians(lat2)) * sin(dlon/2)**2
c = 2 * atan2(sqrt(a), sqrt(1-a))
return R * c
Scoring:
if distance_km < 0.5:
location_score = 40 # Within 500m (walking distance)
elif distance_km < 1.0:
location_score = 30 # Within 1km (short drive)
elif distance_km < 2.0:
location_score = 20 # Within 2km (nearby)
else:
location_score = 0 # Too far
Store lookup:
- Canadian Tire: lat/lon from mock data
- Tim Hortons: lat/lon from mock data
- Sport Chek: lat/lon from mock data
If offer targets multiple brands: Use closest store.
Dimension 2: Time Match (30 points max)
Check if current time matches offer's preferred delivery hours/days:
preferred_hours = offer.delivery_rules.get("preferred_hours", []) # e.g., [9, 10, 11, 14, 15, 16]
preferred_days = offer.delivery_rules.get("preferred_days", []) # e.g., ["monday", "wednesday", "friday"]
current_hour = context.time.hour
current_day = context.time.day_of_week
if current_hour in preferred_hours:
time_score = 30 # Exact hour match
elif current_day in preferred_days:
time_score = 20 # Day match (but not ideal hour)
else:
time_score = 0 # No match
Default preferred hours (if not specified):
- Morning: 8-11 (breakfast/commute)
- Afternoon: 14-17 (after lunch)
- Evening: 18-20 (after work)
Avoid quiet hours: 10pm-8am (score = 0)
Dimension 3: Weather Match (20 points max)
Match weather-dependent offers:
weather_trigger = offer.delivery_rules.get("weather_trigger") # e.g., "cold", "rainy", "hot"
current_weather = context.weather.conditions
if weather_trigger:
if current_weather == weather_trigger:
weather_score = 20 # Exact match (e.g., cold weather → winter gear offer)
else:
weather_score = 0 # No match
else:
weather_score = 10 # Weather-agnostic offer (baseline score)
Weather conditions:
- Cold: Temperature < 0°C → Offers for winter gear, hot beverages
- Hot: Temperature > 25°C → Offers for outdoor activities, cooling products
- Rainy: Precipitation > 0mm → Offers for indoor activities, car care
- Sunny: Clear sky → Offers for outdoor recreation
Dimension 4: Behavior Match (10 points max)
Match offer category to member's recent purchase history:
offer_category = offer.segment.criteria[0] # e.g., "outdoor"
recent_categories = context.behavior.recent_categories # e.g., ["outdoor", "automotive"]
if offer_category in recent_categories:
behavior_score = 10 # Member has shown interest in this category
else:
behavior_score = 0 # New category (cold start)
Additional behavioral signals (bonus points):
- Member is lapsed (>30 days since visit): +5 bonus to reactivation offers
- Member is high-value (>5000 points): +5 bonus to premium offers
Step 4: Calculate Total Score
total_score = location_score + time_score + weather_score + behavior_score
# Apply modifiers
if member.is_lapsed and offer.objective.includes("reactivate"):
total_score += 5
if member.is_high_value and offer.construct.value > 20:
total_score += 5
# Cap at 100
total_score = min(total_score, 100)
Step 5: Rank Offers by Score
scored_offers = [
{"offer_id": offer.offer_id, "score": total_score, "breakdown": {...}}
for offer in active_offers
]
scored_offers.sort(key=lambda x: x["score"], reverse=True)
Step 6: Activation Decision
Threshold: Activate if score > 60
best_offer = scored_offers[0]
if best_offer["score"] > 60:
action = "activate"
notification = create_notification(member_id, best_offer["offer_id"], context)
await send_notification(notification)
await hub.log_activation(member_id, best_offer["offer_id"], best_offer["score"])
else:
action = "queue_for_later"
await hub.queue_offer(member_id, best_offer["offer_id"], reason="score_below_threshold")
Step 7: Output Matching Report
Output format: context-match-report.json
{
"member_id": "member-12345",
"timestamp": "2026-03-26T14:30:00Z",
"context_summary": {
"location": "43.6532,-79.3832 (near Sport Chek Yonge & Dundas)",
"time": "Wednesday 2:30pm",
"weather": "Cold (-5°C), clear sky",
"behavior": "Lapsed 39 days, last purchased Outdoor"
},
"offers_scored": 5,
"top_offer": {
"offer_id": "offer-12345",
"objective": "Reactivate lapsed outdoor enthusiasts",
"total_score": 85,
"score_breakdown": {
"location": 40,
"time": 30,
"weather": 20,
"behavior": 10,
"modifiers": 5
},
"activation_reason": "High proximity + cold weather + lapsed member reactivation"
},
"action": "activate",
"notification_sent": true
}
Configuration
| Parameter | Default | Description |
|---|---|---|
activation_threshold |
60 | Minimum score to activate (0-100) |
max_distance_km |
2.0 | Maximum distance for location match |
quiet_hours_start |
22 | No notifications after this hour (24h format) |
quiet_hours_end |
8 | No notifications before this hour |
lookback_days |
30 | Behavior history window |
enable_weather_matching |
true | Enable weather-based scoring |
Override via environment variables:
CONTEXT_ACTIVATION_THRESHOLD=70
CONTEXT_MAX_DISTANCE_KM=1.5
CONTEXT_QUIET_HOURS_START=23
Scoring Examples
Example 1: High Score (85/100) - Activate
Context:
- Location: 500m from Sport Chek
- Time: Wednesday 2:30pm
- Weather: Cold (-5°C)
- Behavior: Lapsed 39 days, last bought Outdoor
Offer:
- Objective: "Reactivate lapsed outdoor enthusiasts"
- Category: Outdoor
- Delivery: Preferred afternoons, cold weather trigger
- Store: Sport Chek
Score Breakdown:
- Location: 40 (within 500m)
- Time: 30 (Wednesday afternoon = preferred)
- Weather: 20 (cold weather match)
- Behavior: 10 (outdoor category match)
- Modifier: +5 (lapsed member reactivation)
Total: 85/100 → Activate ✅
Notification:
15% off Winter Outdoor Gear at Sport Chek You're just 5 minutes away! Redeem in-store today. Valid for 2 hours.
Example 2: Medium Score (55/100) - Queue
Context:
- Location: 1.8km from Canadian Tire
- Time: Saturday 7:00pm
- Weather: Sunny, warm
- Behavior: Active shopper, last visited 5 days ago
Offer:
- Objective: "Promote Automotive products"
- Category: Automotive
- Delivery: No time preference, any weather
- Store: Canadian Tire
Score Breakdown:
- Location: 20 (within 2km, but not close)
- Time: 20 (Saturday evening = somewhat preferred)
- Weather: 10 (weather-agnostic)
- Behavior: 0 (no recent Automotive purchases)
- Modifier: 0
Total: 50/100 → Queue for later ⏸️
Reason: Score below threshold (60), member not close enough
Example 3: Low Score (10/100) - Discard
Context:
- Location: 5km from Tim Hortons
- Time: Monday 11:00pm
- Weather: Rainy
- Behavior: New member, no purchase history
Offer:
- Objective: "Breakfast combo promotion"
- Category: Food & Beverage
- Delivery: Morning hours (7-10am)
- Store: Tim Hortons
Score Breakdown:
- Location: 0 (too far)
- Time: 0 (quiet hours, wrong time of day)
- Weather: 10 (baseline)
- Behavior: 0 (no history)
- Modifier: 0
Total: 10/100 → Discard ❌
Reason: Multiple mismatches (distance, time, behavior)
Integration with Scout Workflow
sequenceDiagram
participant Member
participant Scout
participant ContextMatcher
participant Hub
participant NotificationService
Member->>Scout: GPS signal (near store)
Scout->>Scout: Gather weather, time, behavior
Scout->>ContextMatcher: Match context to offers
ContextMatcher->>Hub: Fetch active offers
Hub->>ContextMatcher: Return 5 offers
ContextMatcher->>ContextMatcher: Score each offer
alt Score > 60
ContextMatcher->>Scout: Top offer (score: 85)
Scout->>NotificationService: Send push notification
NotificationService->>Member: "15% off Winter Gear nearby"
Scout->>Hub: Log activation
else Score <= 60
ContextMatcher->>Scout: Top offer (score: 55)
Scout->>Hub: Queue for later
Hub->>Hub: Store in queue
end
Output Files
All context matching reports saved to context-match-reports/:
context-match-reports/<member_id>_<timestamp>.json- Full match reportcontext-match-reports/daily_summary.json- Aggregate stats (activations, avg score, top patterns)
Monitoring & Alerts
Metrics to Track
- Activation rate: % of context signals that result in activation
- Average score: Mean score of top-ranked offers
- Score distribution: Histogram of scores (0-20, 21-40, 41-60, 61-80, 81-100)
- Dimension contribution: Which dimensions contribute most to score (location vs time vs weather vs behavior)
Alerts
- Activation rate <5%: Threshold may be too high (tune from 60 to 50)
- Average score <40: Offers not matching context well (review offer design)
- No activations for 1 hour: Check if Scout is receiving context signals
Testing
Unit Tests
# Test location scoring
pytest tests/skills/test_context_matching.py::test_location_score
# Test time scoring
pytest tests/skills/test_context_matching.py::test_time_score
# Test weather matching
pytest tests/skills/test_context_matching.py::test_weather_match
Integration Tests
# Test full workflow: Context signal → Match → Activate
pytest tests/integration/test_scout_activation_workflow.py
Best Practices
- Log all scoring decisions (audit trail + ML training data)
- A/B test threshold values (60 vs 50 vs 70)
- Personalize over time (learn member preferences)
- Respect quiet hours (no notifications 10pm-8am)
- Monitor false positives (activations that don't convert)
Last Updated: 2026-03-26 Version: 1.0 Owner: TriStar Hackathon Team