Skip to content
Closed
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
8 changes: 8 additions & 0 deletions usecases/llm-credit-card-advisor/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
# Dozer + LLM + Vector Search + LangChain Credit Card Advisor Sample

This sample implements the hyper-personalized banking chatbot architecture described in [Dozer's LLM Chatbot article](https://getdozer.io/blog/llm-chatbot) for [getdozer/dozer#1690](https://github.com/getdozer/dozer/issues/1690).

## Architecture
1. **Multi-Dataset Ingestion (`dozer-config.yaml`)**: Joins `customers.csv` and `transactions.csv` into a real-time `customer_360_profile` materialization alongside `credit_cards.csv`.
2. **Real-Time Context Retrieval (`advisor.py`)**: Queries the Dozer `/customer_profile` REST endpoint, filters eligible credit card embeddings/products by live credit score and top spend category, and constructs the grounded LangChain prompt.
3. **Automated Tests (`test_advisor.py`)**: Verified with `pytest`.
110 changes: 110 additions & 0 deletions usecases/llm-credit-card-advisor/advisor.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
import csv
import json
import os
import urllib.request
from typing import Dict, List, Any

DOZER_REST_URL = os.environ.get("DOZER_REST_URL", "http://127.0.0.1:8080")


class DozerProfileClient:
"""Queries real-time unified customer profiles from Dozer REST endpoints with local CSV fallback."""

def __init__(self, base_url: str = DOZER_REST_URL, data_dir: str = None):
self.base_url = base_url.rstrip("/")
self.data_dir = data_dir or os.path.join(os.path.dirname(__file__), "data")

def get_customer_profile(self, customer_id: int) -> Dict[str, Any]:
try:
req = urllib.request.Request(
f"{self.base_url}/customer_profile/query",
data=json.dumps({"$filter": {"customer_id": int(customer_id)}}).encode("utf-8"),
headers={"Content-Type": "application/json"},
method="POST",
)
with urllib.request.urlopen(req, timeout=2) as resp:
rows = json.loads(resp.read().decode("utf-8"))
if rows:
return rows[0]
except Exception:
pass
return self._local_fallback_profile(customer_id)

def _local_fallback_profile(self, customer_id: int) -> Dict[str, Any]:
cid = str(customer_id)
customer = None
with open(os.path.join(self.data_dir, "customers.csv"), encoding="utf-8") as f:
for row in csv.DictReader(f):
if row["customer_id"] == cid:
customer = row
break
if not customer:
raise ValueError(f"Customer {customer_id} not found")

txs = []
with open(os.path.join(self.data_dir, "transactions.csv"), encoding="utf-8") as f:
for row in csv.DictReader(f):
if row["customer_id"] == cid:
txs.append(row)

total_spend = sum(float(t["amount_usd"]) for t in txs)
top_cat = max((t["merchant_category"] for t in txs), default="General")
return {
"customer_id": int(cid),
"full_name": customer["full_name"],
"income_tier": customer["income_tier"],
"credit_score": int(customer["credit_score"]),
"tx_count": len(txs),
"total_spend_usd": round(total_spend, 2),
"top_category": top_cat,
}

def list_credit_cards(self) -> List[Dict[str, Any]]:
cards = []
with open(os.path.join(self.data_dir, "credit_cards.csv"), encoding="utf-8") as f:
for row in csv.DictReader(f):
row["min_credit_score"] = int(row["min_credit_score"])
row["annual_fee_usd"] = float(row["annual_fee_usd"])
cards.append(row)
return cards


def build_hyper_personalized_prompt(profile: Dict[str, Any], matched_cards: List[Dict[str, Any]], user_query: str) -> str:
cards_txt = "\n".join(
f"- {c['card_name']} (Min Score: {c['min_credit_score']}, Fee: ${c['annual_fee_usd']}): {c['rewards_summary']}"
for c in matched_cards
)
return (
f"System: You are a hyper-personalized banking AI assistant powered by Dozer real-time customer profiles.\n"
f"Customer Profile: {profile['full_name']} | Credit Score: {profile['credit_score']} | "
f"Total Spend: ${profile['total_spend_usd']} | Top Category: {profile['top_category']}\n"
f"Eligible Card Products:\n{cards_txt}\n"
f"Customer Question: {user_query}"
)


def recommend_credit_card(customer_id: int, user_query: str, client: DozerProfileClient = None) -> Dict[str, Any]:
client = client or DozerProfileClient()
profile = client.get_customer_profile(customer_id)
all_cards = client.list_credit_cards()
eligible = [c for c in all_cards if profile["credit_score"] >= c["min_credit_score"]]
eligible.sort(key=lambda c: (0 if c["target_category"] == profile["top_category"] else 1, -c["min_credit_score"]))
prompt = build_hyper_personalized_prompt(profile, eligible, user_query)
best = eligible[0] if eligible else None
recommendation = (
f"Hi {profile['full_name']}, based on your Dozer real-time profile (Credit Score {profile['credit_score']} "
f"and ${profile['total_spend_usd']} recent spend primarily in {profile['top_category']}), "
f"we recommend the {best['card_name']}: {best['rewards_summary']}."
if best else "No matching card products found."
)
return {
"customer_profile": profile,
"eligible_cards": eligible,
"langchain_context_prompt": prompt,
"advisor_response": recommendation,
}


if __name__ == "__main__":
out = recommend_credit_card(101, "Which credit card maximizes rewards for my upcoming international flights?")
print(json.dumps(out, indent=2))
3 changes: 3 additions & 0 deletions usecases/llm-credit-card-advisor/data/credit_cards.csv
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
card_id,card_name,min_credit_score,annual_fee_usd,rewards_summary,target_category
cc_sapphire,Apex Travel Reserve,740,450,5x points on flights and luxury hotels plus lounge access,Travel
cc_cashback,Everyday Cashback Plus,670,0,3% cashback on groceries and dining with zero annual fee,Groceries
3 changes: 3 additions & 0 deletions usecases/llm-credit-card-advisor/data/customers.csv
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
customer_id,full_name,income_tier,credit_score
101,Alice Vance,High,785
102,Marco Silva,Medium,710
4 changes: 4 additions & 0 deletions usecases/llm-credit-card-advisor/data/transactions.csv
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
tx_id,customer_id,amount_usd,merchant_category
1,101,1450.00,Travel
2,101,890.00,Dining
3,102,320.00,Groceries
58 changes: 58 additions & 0 deletions usecases/llm-credit-card-advisor/dozer-config.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
app_name: dozer-llm-credit-card-advisor
version: 1
connections:
- config: !LocalStorage
details:
path: ./data
tables:
- config: !CSV
name: customers
path: customers.csv
extension: .csv
- config: !CSV
name: transactions
path: transactions.csv
extension: .csv
- config: !CSV
name: credit_cards
path: credit_cards.csv
extension: .csv

sources:
- name: customers
table_name: customers
connection: LocalStorage
- name: transactions
table_name: transactions
connection: LocalStorage
- name: credit_cards
table_name: credit_cards
connection: LocalStorage

sql: |
SELECT
c.customer_id,
c.full_name,
c.income_tier,
c.credit_score,
COUNT(t.tx_id) AS tx_count,
COALESCE(SUM(t.amount_usd), 0.0) AS total_spend_usd,
MAX(t.merchant_category) AS top_category
INTO customer_360_profile
FROM customers c
LEFT JOIN transactions t ON c.customer_id = t.customer_id
GROUP BY c.customer_id, c.full_name, c.income_tier, c.credit_score;

endpoints:
- name: customer_profile
path: /customer_profile
table_name: customer_360_profile
index:
primary_key:
- customer_id
- name: credit_cards
path: /credit_cards
table_name: credit_cards
index:
primary_key:
- card_id
16 changes: 16 additions & 0 deletions usecases/llm-credit-card-advisor/test_advisor.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
from advisor import DozerProfileClient, recommend_credit_card


def test_high_tier_travel_customer_recommendation():
result = recommend_credit_card(101, "Which card is best for my travel spend?")
assert result["customer_profile"]["full_name"] == "Alice Vance"
assert result["customer_profile"]["top_category"] == "Travel"
assert result["eligible_cards"][0]["card_id"] == "cc_sapphire"
assert "Apex Travel Reserve" in result["advisor_response"]


def test_medium_tier_groceries_customer_recommendation():
result = recommend_credit_card(102, "I want a zero-fee card for groceries.")
assert result["customer_profile"]["full_name"] == "Marco Silva"
assert len(result["eligible_cards"]) == 1
assert result["eligible_cards"][0]["card_id"] == "cc_cashback"