Skip to content

Graph Tools Specification

This document defines a composable tool set for AI agents to interact with the consumer knowledge graph. Each tool is optimized for speed, cacheability, and safe composition.

Design Principles:

  • ✅ User-scoped (all queries filter by user_id)
  • ✅ Fast (<50ms for most queries)
  • ✅ Cacheable (with appropriate TTLs)
  • ✅ Limited results (prevent unbounded queries)
  • ✅ Composable (tools can be combined)

  • User: {user_id, name, zip}
  • Product: {product_id, name, category, brand}
  • Offer: {offer_id, title, description, points, priority, start, end}
  • Retailer: {retailer_id, name, address, city, state, zip}
  • User -[PURCHASED {last, times, avg_interval_days, total_spent}]-> Product
    • Tracks what products user buys and repurchase patterns
  • User -[PURCHASED_AT {last, times, total_spent}]-> Retailer
    • Tracks where user shops (retailer known at purchase time)
  • User -[ELIGIBLE {start}]-> Offer
    • Which offers user can access
  • Offer -[APPLIES_TO]-> Product
    • Which products are included in offer
  • Offer -[AVAILABLE_AT]-> Retailer
    • Which retailers honor the offer (retailer-specific deals)
  • Product -[SIMILAR_TO {score}]-> Product
    • Product similarity graph
  • Products are universal: Not tied to specific retailers
  • Purchases link to both Product AND Retailer: User buys “Product X” at “Retailer Y”
  • Offers can be retailer-specific: Some offers only valid at certain stores
  • No geographic search: Retailers are tracked but proximity queries are not supported

  1. User Context Tools (5 tools) - Understanding user behavior
  2. Product Discovery Tools (3 tools) - Finding products
  3. Temporal Tools (2 tools) - Time-based predictions
  4. Offer Tools (3 tools) - Rewards and promotions
  5. Brand Tools (3 tools) - Brand relationships and preferences
  6. Retailer Tools (2 tools) - Store preferences and offer availability
  7. Collaborative Tools (2 tools) - Social recommendations and trends
  8. Composition Tool (1 tool) - Deep product context

Total: 20 tools


Purpose: Get user’s recent purchases with full details

Query:

MATCH (u:User {user_id: $user_id})-[p:PURCHASED]->(prod:Product)
WHERE p.last >= datetime() - duration({days: $lookback_days})
RETURN {
product_id: prod.product_id,
name: prod.name,
category: prod.category,
brand: prod.brand,
last_purchase: p.last,
times_purchased: p.times,
avg_interval_days: p.avg_interval_days,
total_spent: p.total_spent
} AS purchase
ORDER BY p.last DESC
LIMIT $limit

Parameters:

  • user_id (string, required) - User identifier
  • lookback_days (integer, default: 180) - How far back to look
  • limit (integer, default: 50, max: 100) - Max results

Returns:

[
{
"product_id": "COFFEE_001",
"name": "Colombian Medium Roast Coffee",
"category": "Coffee",
"brand": "Peet's Coffee",
"last_purchase": "2025-01-05T10:30:00Z",
"times_purchased": 12,
"avg_interval_days": 28.5,
"total_spent": 155.88
}
]

Performance:

  • Latency: 5-15ms
  • Cacheable: Yes (5 min TTL)
  • Cache key: purchase_history:{user_id}:{lookback_days}:{limit}

Use Cases:

  • “What have I bought recently?”
  • “Show me my coffee purchases”
  • Understanding user preferences

Purpose: Aggregate user’s purchasing behavior by category

Query:

MATCH (u:User {user_id: $user_id})-[p:PURCHASED]->(prod:Product)
WHERE p.last >= datetime() - duration({days: $lookback_days})
RETURN {
category: prod.category,
product_count: count(DISTINCT prod),
total_purchases: sum(p.times),
total_spent: sum(p.total_spent),
last_purchase: max(p.last),
avg_interval: avg(p.avg_interval_days)
} AS category_stats
ORDER BY total_purchases DESC

Parameters:

  • user_id (string, required)
  • lookback_days (integer, default: 180)

Returns:

[
{
"category": "Coffee",
"product_count": 3,
"total_purchases": 45,
"total_spent": 389.55,
"last_purchase": "2025-01-05T10:30:00Z",
"avg_interval": 28.3
},
{
"category": "Yogurt",
"product_count": 2,
"total_purchases": 28,
"total_spent": 112.00,
"last_purchase": "2025-01-02T14:15:00Z",
"avg_interval": 14.0
}
]

Performance:

  • Latency: 10-20ms
  • Cacheable: Yes (15 min TTL)
  • Cache key: user_categories:{user_id}:{lookback_days}

Use Cases:

  • “What categories do I buy from most?”
  • “How much do I spend on coffee?”
  • Category-level insights

Purpose: User’s brand preferences and loyalty

Query:

MATCH (u:User {user_id: $user_id})-[p:PURCHASED]->(prod:Product)
WHERE p.last >= datetime() - duration({days: $lookback_days})
RETURN {
brand: prod.brand,
product_count: count(DISTINCT prod),
total_purchases: sum(p.times),
total_spent: sum(p.total_spent),
categories: collect(DISTINCT prod.category),
last_purchase: max(p.last)
} AS brand_stats
ORDER BY total_purchases DESC
LIMIT $limit

Parameters:

  • user_id (string, required)
  • lookback_days (integer, default: 180)
  • limit (integer, default: 20, max: 50)

Returns:

[
{
"brand": "Peet's Coffee",
"product_count": 2,
"total_purchases": 18,
"total_spent": 215.82,
"categories": ["Coffee"],
"last_purchase": "2025-01-05T10:30:00Z"
},
{
"brand": "Fage",
"product_count": 3,
"total_purchases": 24,
"total_spent": 95.76,
"categories": ["Yogurt", "Dairy"],
"last_purchase": "2025-01-02T14:15:00Z"
}
]

Performance:

  • Latency: 10-20ms
  • Cacheable: Yes (15 min TTL)
  • Cache key: user_brands:{user_id}:{lookback_days}:{limit}

Use Cases:

  • “What are my favorite brands?”
  • “Do I buy Fage products?”
  • Brand loyalty analysis

Purpose: Identify user’s regular repurchase patterns

Query:

MATCH (u:User {user_id: $user_id})-[p:PURCHASED]->(prod:Product)
WHERE p.times >= $min_purchases
AND p.avg_interval_days > 0
AND p.last >= datetime() - duration({days: $lookback_days})
RETURN {
product_id: prod.product_id,
name: prod.name,
category: prod.category,
brand: prod.brand,
avg_interval_days: p.avg_interval_days,
times_purchased: p.times,
last_purchase: p.last,
next_expected: p.last + duration({days: toInteger(p.avg_interval_days)}),
is_regular: p.times >= 3,
total_spent: p.total_spent
} AS pattern
ORDER BY p.times DESC
LIMIT $limit

Parameters:

  • user_id (string, required)
  • min_purchases (integer, default: 2) - Minimum purchase count
  • lookback_days (integer, default: 365) - Historical window
  • limit (integer, default: 50, max: 100)

Returns:

[
{
"product_id": "COFFEE_001",
"name": "Colombian Medium Roast Coffee",
"category": "Coffee",
"brand": "Peet's Coffee",
"avg_interval_days": 28.5,
"times_purchased": 12,
"last_purchase": "2025-01-05T10:30:00Z",
"next_expected": "2025-02-02T10:30:00Z",
"is_regular": true,
"total_spent": 143.88
}
]

Performance:

  • Latency: 5-10ms
  • Cacheable: Yes (10 min TTL)
  • Cache key: purchase_patterns:{user_id}:{min_purchases}:{lookback_days}:{limit}

Use Cases:

  • “What do I buy regularly?”
  • “What are my staple products?”
  • Understanding routine purchases

Purpose: User’s brand preferences within specific categories

Query:

MATCH (u:User {user_id: $user_id})-[p:PURCHASED]->(prod:Product {category: $category})
WHERE p.last >= datetime() - duration({days: $lookback_days})
WITH prod.brand AS brand,
count(DISTINCT prod) AS product_count,
sum(p.times) AS total_purchases,
sum(p.total_spent) AS total_spent,
max(p.last) AS last_purchase
RETURN {
brand: brand,
product_count: product_count,
total_purchases: total_purchases,
total_spent: total_spent,
last_purchase: last_purchase,
share_of_category: toFloat(total_purchases) /
(SELECT sum(p2.times)
FROM (u)-[p2:PURCHASED]->(:Product {category: $category}))
} AS brand_affinity
ORDER BY total_purchases DESC
LIMIT $limit

Parameters:

  • user_id (string, required)
  • category (string, required) - Product category to analyze
  • lookback_days (integer, default: 180)
  • limit (integer, default: 10, max: 20)

Returns:

[
{
"brand": "Peet's Coffee",
"product_count": 2,
"total_purchases": 18,
"total_spent": 215.82,
"last_purchase": "2025-01-05T10:30:00Z",
"share_of_category": 0.67
},
{
"brand": "Starbucks",
"product_count": 1,
"total_purchases": 9,
"total_spent": 107.91,
"last_purchase": "2024-12-28T09:15:00Z",
"share_of_category": 0.33
}
]

Performance:

  • Latency: 15-25ms
  • Cacheable: Yes (20 min TTL)
  • Cache key: category_brand_affinity:{user_id}:{category}:{lookback_days}

Use Cases:

  • “What coffee brands do I prefer?”
  • “Am I loyal to one brand in this category?”
  • Brand switching analysis

Purpose: Find products with filters and user context

Query:

MATCH (p:Product)
WHERE ($category IS NULL OR p.category = $category)
AND ($brand IS NULL OR p.brand = $brand)
AND ($name_pattern IS NULL OR toLower(p.name) CONTAINS toLower($name_pattern))
OPTIONAL MATCH (u:User {user_id: $user_id})-[purchased:PURCHASED]->(p)
RETURN {
product_id: p.product_id,
name: p.name,
category: p.category,
brand: p.brand,
user_purchased: purchased IS NOT NULL,
user_purchase_count: CASE WHEN purchased IS NOT NULL THEN purchased.times ELSE 0 END,
user_last_purchase: CASE WHEN purchased IS NOT NULL THEN purchased.last ELSE null END
} AS product
ORDER BY user_purchased DESC, p.name ASC
LIMIT $limit

Parameters:

  • user_id (string, required) - For user context
  • category (string, optional) - Filter by category
  • brand (string, optional) - Filter by brand
  • name_pattern (string, optional) - Search in product name (case-insensitive)
  • limit (integer, default: 50, max: 100)

Returns:

[
{
"product_id": "COFFEE_001",
"name": "Colombian Medium Roast Coffee",
"category": "Coffee",
"brand": "Peet's Coffee",
"user_purchased": true,
"user_purchase_count": 12,
"user_last_purchase": "2025-01-05T10:30:00Z"
},
{
"product_id": "COFFEE_ETH",
"name": "Ethiopian Medium Roast",
"category": "Coffee",
"brand": "Peet's Coffee",
"user_purchased": false,
"user_purchase_count": 0,
"user_last_purchase": null
}
]

Performance:

  • Latency: 20-50ms
  • Cacheable: Yes (1 hour TTL, varies by user context)
  • Cache key: search_products:{user_id}:{category}:{brand}:{name_pattern}:{limit}

Use Cases:

  • “Show me all coffee products”
  • “What Fage yogurt products are available?”
  • “Search for ‘organic bread‘“

Purpose: Get products similar to a seed product

Query:

MATCH (seed:Product {product_id: $product_id})-[s:SIMILAR_TO]->(similar:Product)
WHERE s.score >= $min_similarity
OPTIONAL MATCH (u:User {user_id: $user_id})-[p:PURCHASED]->(similar)
RETURN {
product_id: similar.product_id,
name: similar.name,
category: similar.category,
brand: similar.brand,
similarity_score: s.score,
user_purchased: p IS NOT NULL,
user_purchase_count: CASE WHEN p IS NOT NULL THEN p.times ELSE 0 END,
user_last_purchase: CASE WHEN p IS NOT NULL THEN p.last ELSE null END
} AS similar_product
ORDER BY s.score DESC
LIMIT $limit

Parameters:

  • product_id (string, required) - Seed product
  • user_id (string, required) - For user context
  • min_similarity (float, default: 0.6, range: 0.0-1.0) - Minimum similarity threshold
  • limit (integer, default: 20, max: 50)

Returns:

[
{
"product_id": "COFFEE_ETH",
"name": "Ethiopian Medium Roast",
"category": "Coffee",
"brand": "Peet's Coffee",
"similarity_score": 0.89,
"user_purchased": false,
"user_purchase_count": 0,
"user_last_purchase": null
},
{
"product_id": "COFFEE_SUM",
"name": "Sumatra Dark Roast",
"category": "Coffee",
"brand": "Peet's Coffee",
"similarity_score": 0.82,
"user_purchased": true,
"user_purchase_count": 3,
"user_last_purchase": "2024-11-15T08:00:00Z"
}
]

Performance:

  • Latency: 5-10ms
  • Cacheable: Yes (1 hour TTL)
  • Cache key: similar_products:{product_id}:{user_id}:{min_similarity}:{limit}

Use Cases:

  • “Find products like this coffee”
  • “Show me alternatives to my usual yogurt”
  • Product recommendations

Purpose: Find products user hasn’t tried in a category, prioritized by similarity

Query:

MATCH (p:Product {category: $category})
WHERE NOT EXISTS((u:User {user_id: $user_id})-[:PURCHASED]->(p))
AND ($brand IS NULL OR p.brand = $brand)
OPTIONAL MATCH (p)<-[s:SIMILAR_TO]-(tried:Product)<-[:PURCHASED]-(u)
WITH p, max(s.score) AS similarity_to_tried
RETURN {
product_id: p.product_id,
name: p.name,
category: p.category,
brand: p.brand,
similarity_to_tried: COALESCE(similarity_to_tried, 0.0),
relevance_score: COALESCE(similarity_to_tried, 0.0)
} AS alternative
ORDER BY relevance_score DESC, p.name ASC
LIMIT $limit

Parameters:

  • user_id (string, required)
  • category (string, required)
  • brand (string, optional) - Filter to specific brand
  • limit (integer, default: 20, max: 50)

Returns:

[
{
"product_id": "COFFEE_ETH",
"name": "Ethiopian Medium Roast",
"category": "Coffee",
"brand": "Peet's Coffee",
"similarity_to_tried": 0.89,
"relevance_score": 0.89
},
{
"product_id": "COFFEE_KON",
"name": "Kona Blend",
"category": "Coffee",
"brand": "Hawaiian Gold",
"similarity_to_tried": 0.65,
"relevance_score": 0.65
}
]

Performance:

  • Latency: 20-40ms
  • Cacheable: Yes (30 min TTL)
  • Cache key: alternatives:{user_id}:{category}:{brand}:{limit}

Use Cases:

  • “What coffee haven’t I tried yet?”
  • “Show me new products in yogurt category”
  • Exploration and discovery

Purpose: Predict when user will need to buy products again

Query:

MATCH (u:User {user_id: $user_id})-[p:PURCHASED]->(prod:Product)
WHERE p.times >= $min_intervals
AND p.avg_interval_days > 0
WITH prod, p,
p.last + duration({days: toInteger(p.avg_interval_days)}) AS next_expected,
duration.inDays(datetime(), p.last + duration({days: toInteger(p.avg_interval_days)})).days AS days_until
WHERE days_until <= $prediction_window
AND days_until >= $min_days
RETURN {
product_id: prod.product_id,
name: prod.name,
category: prod.category,
brand: prod.brand,
next_expected_date: next_expected,
days_until: days_until,
avg_interval_days: p.avg_interval_days,
times_purchased: p.times,
last_purchase: p.last,
confidence: CASE
WHEN p.times >= 5 THEN 'high'
WHEN p.times >= 3 THEN 'medium'
ELSE 'low'
END
} AS prediction
ORDER BY days_until ASC
LIMIT $limit

Parameters:

  • user_id (string, required)
  • prediction_window (integer, default: 14) - Days ahead to predict
  • min_days (integer, default: 0) - Minimum days (0 = include overdue)
  • min_intervals (integer, default: 2) - Minimum purchase count
  • limit (integer, default: 20, max: 50)

Returns:

[
{
"product_id": "COFFEE_001",
"name": "Colombian Medium Roast Coffee",
"category": "Coffee",
"brand": "Peet's Coffee",
"next_expected_date": "2025-01-09T10:30:00Z",
"days_until": 2,
"avg_interval_days": 28.5,
"times_purchased": 12,
"last_purchase": "2025-01-05T10:30:00Z",
"confidence": "high"
},
{
"product_id": "YOGURT_002",
"name": "Fage Greek Yogurt 0%",
"category": "Yogurt",
"brand": "Fage",
"next_expected_date": "2025-01-09T14:15:00Z",
"days_until": 2,
"avg_interval_days": 14.0,
"times_purchased": 8,
"last_purchase": "2025-01-02T14:15:00Z",
"confidence": "high"
}
]

Performance:

  • Latency: 5-15ms
  • Cacheable: Yes (15 min TTL)
  • Cache key: repurchase_predictions:{user_id}:{prediction_window}:{min_days}:{min_intervals}:{limit}

Use Cases:

  • “What should I buy this week?”
  • “What am I running low on?”
  • Shopping list generation

Purpose: Historical timeline of user’s purchases

Query:

MATCH (u:User {user_id: $user_id})-[p:PURCHASED]->(prod:Product)
WHERE ($category IS NULL OR prod.category = $category)
AND ($brand IS NULL OR prod.brand = $brand)
AND p.last >= datetime() - duration({days: $lookback_days})
RETURN {
product_id: prod.product_id,
name: prod.name,
category: prod.category,
brand: prod.brand,
purchase_date: p.last,
times_purchased: p.times,
avg_interval_days: p.avg_interval_days
} AS timeline_event
ORDER BY p.last DESC
LIMIT $limit

Parameters:

  • user_id (string, required)
  • category (string, optional) - Filter by category
  • brand (string, optional) - Filter by brand
  • lookback_days (integer, default: 180)
  • limit (integer, default: 100, max: 200)

Returns:

[
{
"product_id": "COFFEE_001",
"name": "Colombian Medium Roast Coffee",
"category": "Coffee",
"brand": "Peet's Coffee",
"purchase_date": "2025-01-05T10:30:00Z",
"times_purchased": 12,
"avg_interval_days": 28.5
},
{
"product_id": "YOGURT_002",
"name": "Fage Greek Yogurt 0%",
"category": "Yogurt",
"brand": "Fage",
"purchase_date": "2025-01-02T14:15:00Z",
"times_purchased": 8,
"avg_interval_days": 14.0
}
]

Performance:

  • Latency: 10-20ms
  • Cacheable: Yes (5 min TTL)
  • Cache key: purchase_timeline:{user_id}:{category}:{brand}:{lookback_days}:{limit}

Use Cases:

  • “When did I last buy coffee?”
  • “Show my purchase history for yogurt”
  • Temporal analysis

Purpose: Get offers user is currently eligible for

Query:

MATCH (u:User {user_id: $user_id})-[e:ELIGIBLE]->(o:Offer)
WHERE o.start <= datetime()
AND datetime() <= o.end
AND e.start <= datetime()
OPTIONAL MATCH (o)-[:APPLIES_TO]->(p:Product)
WHERE ($category IS NULL OR p.category = $category)
WITH o, collect(DISTINCT p.product_id) AS applicable_products
WHERE size(applicable_products) > 0 OR $category IS NULL
RETURN {
offer_id: o.offer_id,
title: o.title,
description: o.description,
points: o.points,
priority: o.priority,
start_date: o.start,
end_date: o.end,
days_remaining: duration.inDays(datetime(), o.end).days,
applicable_product_ids: applicable_products,
product_count: size(applicable_products)
} AS offer
ORDER BY o.points DESC, o.priority DESC, o.end ASC
LIMIT $limit

Parameters:

  • user_id (string, required)
  • category (string, optional) - Filter to offers in specific category
  • limit (integer, default: 20, max: 50)

Returns:

[
{
"offer_id": "OFFER_001",
"title": "500 bonus points on coffee",
"description": "Earn 500 points on any coffee purchase",
"points": 500,
"priority": 1,
"start_date": "2025-01-05T00:00:00Z",
"end_date": "2025-01-10T23:59:59Z",
"days_remaining": 3,
"applicable_product_ids": ["COFFEE_001", "COFFEE_002", "COFFEE_ETH"],
"product_count": 3
},
{
"offer_id": "OFFER_015",
"title": "Double points on dairy",
"description": "2x points on all dairy products",
"points": 200,
"priority": 2,
"start_date": "2025-01-07T00:00:00Z",
"end_date": "2025-01-14T23:59:59Z",
"days_remaining": 7,
"applicable_product_ids": ["YOGURT_002", "MILK_001", "CHEESE_003"],
"product_count": 3
}
]

Performance:

  • Latency: 10-30ms
  • Cacheable: Yes (5 min TTL)
  • Cache key: active_offers:{user_id}:{category}:{limit}

Use Cases:

  • “What offers do I have?”
  • “Are there any coffee deals?”
  • Offer discovery

Purpose: Check which specific products have active offers

Query:

MATCH (u:User {user_id: $user_id})
UNWIND $product_ids AS product_id
MATCH (p:Product {product_id: product_id})
OPTIONAL MATCH (u)-[:ELIGIBLE]->(o:Offer)-[:APPLIES_TO]->(p)
WHERE o.start <= datetime() AND datetime() <= o.end
RETURN {
product_id: p.product_id,
name: p.name,
category: p.category,
brand: p.brand,
has_offer: o IS NOT NULL,
offer: CASE WHEN o IS NOT NULL THEN {
offer_id: o.offer_id,
title: o.title,
points: o.points,
end_date: o.end,
days_remaining: duration.inDays(datetime(), o.end).days
} ELSE null END
} AS product_offer
ORDER BY has_offer DESC, p.name ASC

Parameters:

  • user_id (string, required)
  • product_ids (array of strings, required) - Products to check

Returns:

[
{
"product_id": "COFFEE_001",
"name": "Colombian Medium Roast Coffee",
"category": "Coffee",
"brand": "Peet's Coffee",
"has_offer": true,
"offer": {
"offer_id": "OFFER_001",
"title": "500 bonus points on coffee",
"points": 500,
"end_date": "2025-01-10T23:59:59Z",
"days_remaining": 3
}
},
{
"product_id": "BREAD_005",
"name": "Dave's Killer Bread - 21 Grain",
"category": "Bread",
"brand": "Dave's Killer Bread",
"has_offer": false,
"offer": null
}
]

Performance:

  • Latency: 5-10ms
  • Cacheable: No (product_ids are dynamic)

Use Cases:

  • “Does my coffee have an offer?”
  • “Which items in my list have rewards?”
  • Shopping list optimization

Purpose: Search all available offers with filters

Query:

MATCH (u:User {user_id: $user_id})-[e:ELIGIBLE]->(o:Offer)
WHERE o.start <= datetime()
AND datetime() <= o.end
AND e.start <= datetime()
AND ($min_points IS NULL OR o.points >= $min_points)
AND ($search_term IS NULL OR
toLower(o.title) CONTAINS toLower($search_term) OR
toLower(o.description) CONTAINS toLower($search_term))
// Optional retailer filter
OPTIONAL MATCH (o)-[:AVAILABLE_AT]->(r:Retailer)
WHERE ($retailer_id IS NULL OR r.retailer_id = $retailer_id)
WITH o, collect(DISTINCT r.retailer_id) AS retailer_ids
WHERE $retailer_id IS NULL OR $retailer_id IN retailer_ids
// Get applicable products
OPTIONAL MATCH (o)-[:APPLIES_TO]->(p:Product)
WHERE ($category IS NULL OR p.category = $category)
AND ($brand IS NULL OR p.brand = $brand)
WITH o, retailer_ids, collect(DISTINCT {
product_id: p.product_id,
name: p.name,
category: p.category,
brand: p.brand
}) AS applicable_products
WHERE size(applicable_products) > 0 OR
($category IS NULL AND $brand IS NULL)
RETURN {
offer_id: o.offer_id,
title: o.title,
description: o.description,
points: o.points,
priority: o.priority,
start_date: o.start,
end_date: o.end,
days_remaining: duration.inDays(datetime(), o.end).days,
applicable_products: applicable_products,
product_count: size(applicable_products),
retailer_ids: retailer_ids,
is_retailer_specific: size(retailer_ids) > 0
} AS offer
ORDER BY o.points DESC, o.priority DESC, o.end ASC
LIMIT $limit

Parameters:

  • user_id (string, required)
  • category (string, optional) - Filter to category
  • brand (string, optional) - Filter to brand
  • retailer_id (string, optional) - Filter to specific retailer
  • min_points (integer, optional) - Minimum points threshold
  • search_term (string, optional) - Search in title/description
  • limit (integer, default: 20, max: 50)

Returns:

[
{
"offer_id": "OFFER_001",
"title": "500 bonus points on coffee",
"description": "Earn 500 points on any coffee purchase from Peet's, Starbucks, or local brands",
"points": 500,
"priority": 1,
"start_date": "2025-01-05T00:00:00Z",
"end_date": "2025-01-10T23:59:59Z",
"days_remaining": 3,
"applicable_products": [
{
"product_id": "COFFEE_001",
"name": "Colombian Medium Roast Coffee",
"category": "Coffee",
"brand": "Peet's Coffee"
},
{
"product_id": "COFFEE_002",
"name": "French Roast",
"category": "Coffee",
"brand": "Peet's Coffee"
}
],
"product_count": 2,
"retailer_ids": ["WALMART_001", "TARGET_005"],
"is_retailer_specific": true
}
]

Performance:

  • Latency: 15-40ms
  • Cacheable: Yes (5 min TTL)
  • Cache key: search_offers:{user_id}:{category}:{brand}:{retailer_id}:{min_points}:{search_term}:{limit}

Use Cases:

  • “Show me all high-value offers”
  • “Find offers with ‘bonus’ in the title”
  • “What Peet’s Coffee offers are available?”
  • “What offers are valid at Walmart?”
  • Advanced offer discovery

Purpose: Get all products from a specific brand

Query:

MATCH (p:Product {brand: $brand})
WHERE ($category IS NULL OR p.category = $category)
OPTIONAL MATCH (u:User {user_id: $user_id})-[purchased:PURCHASED]->(p)
RETURN {
product_id: p.product_id,
name: p.name,
category: p.category,
user_purchased: purchased IS NOT NULL,
user_purchase_count: CASE WHEN purchased IS NOT NULL THEN purchased.times ELSE 0 END,
user_last_purchase: CASE WHEN purchased IS NOT NULL THEN purchased.last ELSE null END,
user_total_spent: CASE WHEN purchased IS NOT NULL THEN purchased.total_spent ELSE 0 END
} AS product
ORDER BY user_purchased DESC, p.category ASC, p.name ASC
LIMIT $limit

Parameters:

  • brand (string, required) - Brand name
  • user_id (string, required) - For user context
  • category (string, optional) - Filter to category
  • limit (integer, default: 50, max: 100)

Returns:

[
{
"product_id": "COFFEE_001",
"name": "Colombian Medium Roast Coffee",
"category": "Coffee",
"user_purchased": true,
"user_purchase_count": 12,
"user_last_purchase": "2025-01-05T10:30:00Z",
"user_total_spent": 143.88
},
{
"product_id": "COFFEE_002",
"name": "French Roast",
"category": "Coffee",
"user_purchased": true,
"user_purchase_count": 6,
"user_last_purchase": "2024-12-20T11:00:00Z",
"user_total_spent": 71.94
},
{
"product_id": "COFFEE_ETH",
"name": "Ethiopian Medium Roast",
"category": "Coffee",
"user_purchased": false,
"user_purchase_count": 0,
"user_last_purchase": null,
"user_total_spent": 0
}
]

Performance:

  • Latency: 15-30ms
  • Cacheable: Yes (30 min TTL)
  • Cache key: brand_products:{brand}:{user_id}:{category}:{limit}

Use Cases:

  • “Show me all Peet’s Coffee products”
  • “What Fage products are available?”
  • Brand exploration

Purpose: Compare user’s relationship with multiple brands

Query:

MATCH (u:User {user_id: $user_id})
UNWIND $brands AS brand
OPTIONAL MATCH (u)-[p:PURCHASED]->(prod:Product {brand: brand})
WHERE ($category IS NULL OR prod.category = $category)
WITH brand,
count(DISTINCT prod) AS product_count,
sum(p.times) AS total_purchases,
sum(p.total_spent) AS total_spent,
max(p.last) AS last_purchase,
avg(p.avg_interval_days) AS avg_interval
RETURN {
brand: brand,
product_count: COALESCE(product_count, 0),
total_purchases: COALESCE(total_purchases, 0),
total_spent: COALESCE(total_spent, 0.0),
last_purchase: last_purchase,
avg_interval: avg_interval,
is_purchased: product_count > 0
} AS brand_comparison
ORDER BY total_purchases DESC

Parameters:

  • user_id (string, required)
  • brands (array of strings, required) - Brands to compare
  • category (string, optional) - Filter to category

Returns:

[
{
"brand": "Peet's Coffee",
"product_count": 2,
"total_purchases": 18,
"total_spent": 215.82,
"last_purchase": "2025-01-05T10:30:00Z",
"avg_interval": 28.5,
"is_purchased": true
},
{
"brand": "Starbucks",
"product_count": 1,
"total_purchases": 9,
"total_spent": 107.91,
"last_purchase": "2024-12-28T09:15:00Z",
"avg_interval": 32.0,
"is_purchased": true
},
{
"brand": "Lavazza",
"product_count": 0,
"total_purchases": 0,
"total_spent": 0.0,
"last_purchase": null,
"avg_interval": null,
"is_purchased": false
}
]

Performance:

  • Latency: 20-40ms
  • Cacheable: Yes (15 min TTL)
  • Cache key: compare_brands:{user_id}:{brands_hash}:{category}

Use Cases:

  • “Compare Peet’s vs Starbucks for my purchases”
  • “Which yogurt brand do I buy more?”
  • Brand preference analysis

Purpose: Find all brands in a category, ranked by user preference

Query:

MATCH (p:Product {category: $category})
WITH DISTINCT p.brand AS brand
ORDER BY brand
OPTIONAL MATCH (u:User {user_id: $user_id})-[purchased:PURCHASED]->(prod:Product {brand: brand, category: $category})
WITH brand,
count(DISTINCT prod) AS products_purchased,
sum(purchased.times) AS total_purchases,
sum(purchased.total_spent) AS total_spent,
max(purchased.last) AS last_purchase
RETURN {
brand: brand,
products_purchased: COALESCE(products_purchased, 0),
total_purchases: COALESCE(total_purchases, 0),
total_spent: COALESCE(total_spent, 0.0),
last_purchase: last_purchase,
user_loyalty: CASE
WHEN total_purchases >= 10 THEN 'high'
WHEN total_purchases >= 3 THEN 'medium'
WHEN total_purchases >= 1 THEN 'low'
ELSE 'never_purchased'
END
} AS brand
ORDER BY total_purchases DESC NULLS LAST, brand ASC
LIMIT $limit

Parameters:

  • user_id (string, required)
  • category (string, required)
  • limit (integer, default: 20, max: 50)

Returns:

[
{
"brand": "Peet's Coffee",
"products_purchased": 2,
"total_purchases": 18,
"total_spent": 215.82,
"last_purchase": "2025-01-05T10:30:00Z",
"user_loyalty": "high"
},
{
"brand": "Starbucks",
"products_purchased": 1,
"total_purchases": 9,
"total_spent": 107.91,
"last_purchase": "2024-12-28T09:15:00Z",
"user_loyalty": "medium"
},
{
"brand": "Lavazza",
"products_purchased": 0,
"total_purchases": 0,
"total_spent": 0.0,
"last_purchase": null,
"user_loyalty": "never_purchased"
}
]

Performance:

  • Latency: 25-45ms
  • Cacheable: Yes (20 min TTL)
  • Cache key: discover_brands:{user_id}:{category}:{limit}

Use Cases:

  • “What coffee brands are available?”
  • “Show me yogurt brands I haven’t tried”
  • Category brand exploration

Purpose: Get retailers where user has shopped, ranked by frequency

Query:

MATCH (u:User {user_id: $user_id})-[pa:PURCHASED_AT]->(r:Retailer)
WHERE pa.last >= datetime() - duration({days: $lookback_days})
RETURN {
retailer_id: r.retailer_id,
name: r.name,
address: r.address,
city: r.city,
state: r.state,
zip: r.zip,
times_visited: pa.times,
last_visit: pa.last,
total_spent: pa.total_spent
} AS retailer
ORDER BY pa.times DESC, pa.last DESC
LIMIT $limit

Parameters:

  • user_id (string, required)
  • lookback_days (integer, default: 365)
  • limit (integer, default: 20, max: 50)

Returns:

[
{
"retailer_id": "WALMART_001",
"name": "Walmart Supercenter",
"address": "123 Main St",
"city": "Springfield",
"state": "IL",
"zip": "62701",
"times_visited": 45,
"last_visit": "2025-01-05T18:30:00Z",
"total_spent": 1250.45
},
{
"retailer_id": "TARGET_005",
"name": "Target",
"address": "456 Oak Ave",
"city": "Springfield",
"state": "IL",
"zip": "62702",
"times_visited": 28,
"last_visit": "2024-12-30T14:15:00Z",
"total_spent": 845.20
}
]

Performance:

  • Latency: 15-30ms
  • Cacheable: Yes (15 min TTL)
  • Cache key: user_retailers:{user_id}:{lookback_days}:{limit}

Use Cases:

  • “Where do I usually shop?”
  • “What’s my favorite store?”
  • Understanding shopping patterns

Purpose: Get active offers at a specific retailer

Query:

MATCH (u:User {user_id: $user_id})-[:ELIGIBLE]->(o:Offer)-[:AVAILABLE_AT]->(r:Retailer {retailer_id: $retailer_id})
WHERE o.start <= datetime()
AND datetime() <= o.end
OPTIONAL MATCH (o)-[:APPLIES_TO]->(p:Product)
WHERE ($category IS NULL OR p.category = $category)
WITH o, collect(DISTINCT p.product_id) AS applicable_products
WHERE size(applicable_products) > 0 OR $category IS NULL
RETURN {
offer_id: o.offer_id,
title: o.title,
description: o.description,
points: o.points,
priority: o.priority,
start_date: o.start,
end_date: o.end,
days_remaining: duration.inDays(datetime(), o.end).days,
applicable_product_ids: applicable_products,
product_count: size(applicable_products)
} AS offer
ORDER BY o.points DESC, o.priority DESC, o.end ASC
LIMIT $limit

Parameters:

  • user_id (string, required)
  • retailer_id (string, required)
  • category (string, optional) - Filter to category
  • limit (integer, default: 20, max: 50)

Returns:

[
{
"offer_id": "OFFER_WALMART_500",
"title": "500 points on coffee at Walmart",
"description": "Exclusive Walmart offer - earn 500 points on any coffee purchase",
"points": 500,
"priority": 1,
"start_date": "2025-01-05T00:00:00Z",
"end_date": "2025-01-10T23:59:59Z",
"days_remaining": 3,
"applicable_product_ids": ["COFFEE_001", "COFFEE_002", "COFFEE_ETH"],
"product_count": 3
}
]

Performance:

  • Latency: 15-35ms
  • Cacheable: Yes (5 min TTL)
  • Cache key: retailer_offers:{user_id}:{retailer_id}:{category}:{limit}

Use Cases:

  • “What offers are available at Walmart?”
  • “Show me Target coffee deals”
  • Store-specific offer discovery

Purpose: Find products that similar users have purchased (“people like me”)

Query:

MATCH (me:User {user_id: $user_id})
// Step 1: Find products I've bought (optionally in specific categories)
MATCH (me)-[:PURCHASED]->(my_products:Product)
WHERE ($categories IS NULL OR my_products.category IN $categories)
// Step 2: Find other users who bought those same products
MATCH (similar_user:User)-[:PURCHASED]->(my_products)
WHERE similar_user.user_id <> $user_id
AND ($same_zip_only = false OR similar_user.zip = me.zip)
// Step 3: Count how many products we have in common
WITH me, similar_user, count(DISTINCT my_products) AS shared_products
WHERE shared_products >= $min_shared_products
// Step 4: Find what they bought that I haven't
MATCH (similar_user)-[p:PURCHASED]->(their_product:Product)
WHERE NOT EXISTS((me)-[:PURCHASED]->(their_product))
AND ($category IS NULL OR their_product.category = $category)
// Step 5: Aggregate by product, weight by user similarity
WITH their_product,
count(DISTINCT similar_user) AS buyer_count,
sum(shared_products) AS similarity_score,
avg(p.times) AS avg_repurchases
RETURN {
product_id: their_product.product_id,
name: their_product.name,
category: their_product.category,
brand: their_product.brand,
buyer_count: buyer_count,
similarity_score: similarity_score,
avg_repurchases: avg_repurchases,
recommendation_strength: buyer_count * similarity_score
} AS recommendation
ORDER BY recommendation_strength DESC, buyer_count DESC
LIMIT $limit

Parameters:

  • user_id (string, required)
  • categories (array of strings, optional) - Focus similarity on specific categories
  • same_zip_only (boolean, default: false) - Only include users from same zip code
  • min_shared_products (integer, default: 2) - Minimum overlap to consider users similar
  • category (string, optional) - Only recommend products in this category
  • limit (integer, default: 20, max: 50)

Returns:

[
{
"product_id": "COFFEE_ETH",
"name": "Ethiopian Medium Roast",
"category": "Coffee",
"brand": "Peet's Coffee",
"buyer_count": 18,
"similarity_score": 84,
"avg_repurchases": 3.2,
"recommendation_strength": 1512
},
{
"product_id": "YOGURT_005",
"name": "Chobani Greek Yogurt",
"category": "Yogurt",
"brand": "Chobani",
"buyer_count": 12,
"similarity_score": 58,
"avg_repurchases": 5.1,
"recommendation_strength": 696
}
]

Performance:

  • Latency: 50-150ms (depends on user base size and overlap)
  • Cacheable: Yes (20 min TTL)
  • Cache key: collab_recs:{user_id}:{categories_hash}:{same_zip_only}:{category}:{limit}

Use Cases:

  • “What do people like me buy?”
  • “Show me products similar shoppers purchase”
  • “What coffee do people with my taste buy?”
  • Personalized discovery

Performance Notes:

  • Can be expensive at scale (cross-user traversals)
  • Consider caching aggressively
  • May want to limit to users with recent activity
  • Consider adding time filter on purchases for similarity matching

Purpose: Find popular products in user’s zip code that they haven’t tried

Query:

MATCH (me:User {user_id: $user_id})
MATCH (local_user:User {zip: me.zip})-[p:PURCHASED]->(prod:Product)
WHERE local_user.user_id <> $user_id
AND ($category IS NULL OR prod.category = $category)
AND p.last >= datetime() - duration({days: $lookback_days})
// Exclude products the requesting user has bought
OPTIONAL MATCH (me)-[:PURCHASED]->(prod)
WITH prod, count(DISTINCT local_user) AS local_buyers, me
WHERE me IS NULL // Haven't bought it
RETURN {
product_id: prod.product_id,
name: prod.name,
category: prod.category,
brand: prod.brand,
local_buyers: local_buyers,
popularity_score: local_buyers
} AS trending
ORDER BY local_buyers DESC
LIMIT $limit

Parameters:

  • user_id (string, required)
  • category (string, optional) - Filter to specific category
  • lookback_days (integer, default: 90) - Time window for “trending”
  • limit (integer, default: 20, max: 50)

Returns:

[
{
"product_id": "COFFEE_LOCAL",
"name": "Local Roasters House Blend",
"category": "Coffee",
"brand": "Springfield Roasters",
"local_buyers": 45,
"popularity_score": 45
},
{
"product_id": "BREAD_ARTISAN",
"name": "Artisan Sourdough",
"category": "Bread",
"brand": "Local Bakery",
"local_buyers": 32,
"popularity_score": 32
}
]

Performance:

  • Latency: 30-80ms (depends on zip code user density)
  • Cacheable: Yes (30 min TTL)
  • Cache key: local_trending:{zip}:{category}:{lookback_days}:{limit}

Use Cases:

  • “What’s popular in my area?”
  • “What do people in my neighborhood buy?”
  • “Show me local favorites”
  • Geographic discovery

Performance Notes:

  • Much faster than collaborative filtering
  • Can be cached at zip code level (shared across users)
  • Consider indexing User.zip for better performance

Purpose: Get comprehensive context about a product for a user

Query:

MATCH (p:Product {product_id: $product_id})
OPTIONAL MATCH (u:User {user_id: $user_id})-[purchased:PURCHASED]->(p)
OPTIONAL MATCH (p)-[s:SIMILAR_TO]->(similar:Product)
WHERE s.score >= $similarity_threshold
WITH p, purchased, collect({
product_id: similar.product_id,
name: similar.name,
category: similar.category,
brand: similar.brand,
score: s.score
})[0..$max_similar] AS similar_products
OPTIONAL MATCH (u:User {user_id: $user_id})-[:ELIGIBLE]->(o:Offer)-[:APPLIES_TO]->(p)
WHERE o.start <= datetime() AND datetime() <= o.end
WITH p, purchased, similar_products, collect({
offer_id: o.offer_id,
title: o.title,
description: o.description,
points: o.points,
end_date: o.end,
days_remaining: duration.inDays(datetime(), o.end).days
}) AS active_offers
RETURN {
product_id: p.product_id,
name: p.name,
category: p.category,
brand: p.brand,
user_history: CASE WHEN purchased IS NOT NULL THEN {
times_purchased: purchased.times,
last_purchase: purchased.last,
avg_interval_days: purchased.avg_interval_days,
total_spent: purchased.total_spent,
next_expected: CASE
WHEN purchased.avg_interval_days > 0
THEN purchased.last + duration({days: toInteger(purchased.avg_interval_days)})
ELSE null
END,
days_until_next: CASE
WHEN purchased.avg_interval_days > 0
THEN duration.inDays(datetime(), purchased.last + duration({days: toInteger(purchased.avg_interval_days)})).days
ELSE null
END
} ELSE null END,
similar_products: similar_products,
active_offers: active_offers,
has_active_offers: size(active_offers) > 0
} AS product_context

Parameters:

  • product_id (string, required)
  • user_id (string, required)
  • similarity_threshold (float, default: 0.7)
  • max_similar (integer, default: 5)

Returns:

{
"product_id": "COFFEE_001",
"name": "Colombian Medium Roast Coffee",
"category": "Coffee",
"brand": "Peet's Coffee",
"user_history": {
"times_purchased": 12,
"last_purchase": "2025-01-05T10:30:00Z",
"avg_interval_days": 28.5,
"total_spent": 143.88,
"next_expected": "2025-02-02T10:30:00Z",
"days_until_next": 26
},
"similar_products": [
{
"product_id": "COFFEE_ETH",
"name": "Ethiopian Medium Roast",
"category": "Coffee",
"brand": "Peet's Coffee",
"score": 0.89
},
{
"product_id": "COFFEE_SUM",
"name": "Sumatra Dark Roast",
"category": "Coffee",
"brand": "Peet's Coffee",
"score": 0.82
}
],
"active_offers": [
{
"offer_id": "OFFER_001",
"title": "500 bonus points on coffee",
"description": "Earn 500 points on any coffee purchase",
"points": 500,
"end_date": "2025-01-10T23:59:59Z",
"days_remaining": 3
}
],
"has_active_offers": true
}

Performance:

  • Latency: 15-30ms
  • Cacheable: Yes (30 min TTL)
  • Cache key: product_context:{product_id}:{user_id}:{similarity_threshold}:{max_similar}

Use Cases:

  • “Tell me everything about this coffee”
  • “Should I buy this product?”
  • Deep product exploration

Example 1: “What should I buy this week?”

Section titled “Example 1: “What should I buy this week?””
# Step 1: Get repurchase predictions
predictions = tools.predict_repurchases(
user_id='USER123',
prediction_window=7,
min_days=0
)
# Step 2: Check which products have offers
product_ids = [p['product_id'] for p in predictions]
products_with_offers = tools.match_products_to_offers(
user_id='USER123',
product_ids=product_ids
)
# Step 3: Combine results
shopping_list = []
for product in products_with_offers:
pred = next(p for p in predictions if p['product_id'] == product['product_id'])
shopping_list.append({
**product,
'days_until': pred['days_until'],
'priority': 'high' if pred['days_until'] <= 2 else 'medium'
})
# Sort by urgency and offer value
shopping_list.sort(key=lambda x: (
x['days_until'],
-x.get('offer', {}).get('points', 0)
))

Example 2: “Show me alternatives to my usual coffee”

Section titled “Example 2: “Show me alternatives to my usual coffee””
# Step 1: Get user's coffee purchase history
history = tools.get_user_purchase_history(
user_id='USER123',
lookback_days=90
)
# Filter to coffee category
coffee_purchases = [h for h in history if h['category'] == 'Coffee']
# Find most purchased
usual_coffee = max(coffee_purchases, key=lambda x: x['times_purchased'])
# Step 2: Find similar products
similar = tools.find_similar_products(
product_id=usual_coffee['product_id'],
user_id='USER123',
min_similarity=0.75
)
# Step 3: Check for offers
similar_ids = [s['product_id'] for s in similar if not s['user_purchased']]
with_offers = tools.match_products_to_offers(
user_id='USER123',
product_ids=similar_ids
)
# Combine
alternatives = []
for product in with_offers:
sim = next(s for s in similar if s['product_id'] == product['product_id'])
alternatives.append({
**product,
'similarity_score': sim['similarity_score'],
'recommendation_reason': f"{int(sim['similarity_score']*100)}% similar to {usual_coffee['name']}"
})
# Sort by offer points, then similarity
alternatives.sort(key=lambda x: (
-x.get('offer', {}).get('points', 0),
-x['similarity_score']
))

Example 3: “Compare my spending on Peet’s vs Starbucks”

Section titled “Example 3: “Compare my spending on Peet’s vs Starbucks””
# Use compare_brands tool
comparison = tools.compare_brands(
user_id='USER123',
brands=['Peet\'s Coffee', 'Starbucks'],
category='Coffee'
)
# Get brand products for context
peets_products = tools.get_brand_products(
brand='Peet\'s Coffee',
user_id='USER123',
category='Coffee'
)
starbucks_products = tools.get_brand_products(
brand='Starbucks',
user_id='USER123',
category='Coffee'
)
# Synthesize
report = {
'comparison': comparison,
'details': {
'peets': {
'stats': comparison[0],
'products': peets_products
},
'starbucks': {
'stats': comparison[1],
'products': starbucks_products
}
}
}

Example 4: “Build shopping list optimized for rewards”

Section titled “Example 4: “Build shopping list optimized for rewards””
# Step 1: Get predictions for next 2 weeks
predictions = tools.predict_repurchases(
user_id='USER123',
prediction_window=14
)
# Step 2: Get all active offers
offers = tools.get_active_offers(
user_id='USER123'
)
# Step 3: Match predictions to offers
predicted_ids = [p['product_id'] for p in predictions]
matched = tools.match_products_to_offers(
user_id='USER123',
product_ids=predicted_ids
)
# Step 4: Calculate value score
shopping_list = []
for product in matched:
pred = next(p for p in predictions if p['product_id'] == product['product_id'])
# Value score: urgency + offer points
urgency_score = max(0, 14 - pred['days_until']) / 14 # 0-1
offer_score = product.get('offer', {}).get('points', 0) / 500 # normalize
value_score = (urgency_score * 0.6) + (offer_score * 0.4)
shopping_list.append({
**product,
'days_until': pred['days_until'],
'urgency': 'high' if pred['days_until'] <= 3 else 'medium',
'value_score': value_score
})
# Sort by value score
shopping_list.sort(key=lambda x: -x['value_score'])
# Calculate total points
total_points = sum(p.get('offer', {}).get('points', 0) for p in shopping_list)

ToolAvg LatencyCacheableCache TTLMax Results
get_user_purchase_history5-15ms5 min100
get_user_categories10-20ms15 min20
get_user_brands10-20ms15 min50
get_purchase_patterns5-10ms10 min100
get_category_brand_affinity15-25ms20 min20
search_products20-50ms1 hour100
find_similar_products5-10ms1 hour50
find_alternatives_in_category20-40ms30 min50
predict_repurchases5-15ms15 min50
get_purchase_timeline10-20ms5 min200
get_active_offers10-30ms5 min50
match_products_to_offers5-10ms-50
search_offers15-40ms5 min50
get_brand_products15-30ms30 min100
compare_brands20-40ms15 min10
discover_brands_in_category25-45ms20 min50
get_user_retailers15-30ms15 min50
get_retailer_offers15-35ms5 min50
get_collaborative_recommendations50-150ms20 min50
get_local_trending_products30-80ms30 min50
get_product_context15-30ms30 min1

package tools
import (
"context"
"time"
"github.com/neo4j/neo4j-go-driver/v5/neo4j"
)
type GraphTools struct {
driver neo4j.DriverWithContext
cache CacheClient
userID string
}
func NewGraphTools(driver neo4j.DriverWithContext, cache CacheClient, userID string) *GraphTools {
return &GraphTools{
driver: driver,
cache: cache,
userID: userID,
}
}
// Tool: get_user_purchase_history
func (gt *GraphTools) GetUserPurchaseHistory(ctx context.Context, lookbackDays int, limit int) ([]PurchaseHistoryItem, error) {
// Set defaults
if lookbackDays == 0 {
lookbackDays = 180
}
if limit == 0 || limit > 100 {
limit = 50
}
// Check cache
cacheKey := fmt.Sprintf("purchase_history:%s:%d:%d", gt.userID, lookbackDays, limit)
if cached, found := gt.cache.Get(cacheKey); found {
return cached.([]PurchaseHistoryItem), nil
}
// Execute query
query := `
MATCH (u:User {user_id: $user_id})-[p:PURCHASED]->(prod:Product)
WHERE p.last >= datetime() - duration({days: $lookback_days})
RETURN {
product_id: prod.product_id,
name: prod.name,
category: prod.category,
brand: prod.brand,
last_purchase: p.last,
times_purchased: p.times,
avg_interval_days: p.avg_interval_days,
total_spent: p.total_spent
} AS purchase
ORDER BY p.last DESC
LIMIT $limit
`
session := gt.driver.NewSession(ctx, neo4j.SessionConfig{AccessMode: neo4j.AccessModeRead})
defer session.Close(ctx)
result, err := session.Run(ctx, query, map[string]interface{}{
"user_id": gt.userID,
"lookback_days": lookbackDays,
"limit": limit,
})
if err != nil {
return nil, err
}
var items []PurchaseHistoryItem
for result.Next(ctx) {
record := result.Record()
purchase, _ := record.Get("purchase")
// Parse into PurchaseHistoryItem struct
// ...
items = append(items, item)
}
// Cache result
gt.cache.Set(cacheKey, items, 5*time.Minute)
return items, nil
}
// Implement remaining 15 tools...

All tools enforce:

  1. User scoping: Every query filters by $user_id
  2. Result limits: All queries have LIMIT clauses
  3. Timeout: 5-second query timeout
  4. Parameter validation: Required params checked, ranges enforced
  5. Cache invalidation: Triggered by purchase events

  1. Implement in Go: Create pkg/graph/tools/ package with all 20 tools
  2. Add caching: Integrate Redis for caching layer with tool-specific TTLs
  3. Agent integration: Expose as function-calling tools to LLM
  4. Monitoring: Track tool usage, latency, cache hit rates
  5. Optimization: Add indexes based on actual query patterns
    • Index on User.zip for collaborative filtering and trending queries
    • Consider composite indexes for category + brand lookups
  6. Schema updates: Add Retailer nodes and PURCHASED_AT, AVAILABLE_AT relationships
  7. Collaborative filtering optimization: Monitor performance at scale, consider pre-computation if needed