Update UI and expanded cards
Browse files
app.py
CHANGED
|
@@ -10,9 +10,10 @@ from sentence_transformers import CrossEncoder
|
|
| 10 |
import gradio as gr
|
| 11 |
import tempfile
|
| 12 |
import re
|
|
|
|
| 13 |
|
| 14 |
#for adding bank name to the cards in the graph
|
| 15 |
-
eligibility_df = pd.read_csv("
|
| 16 |
card_to_bank = dict(zip(eligibility_df['Name'], eligibility_df['Bank']))
|
| 17 |
|
| 18 |
#neo4j credentials
|
|
@@ -37,7 +38,7 @@ def generate_cypher(user_query, query_intent, include_cobranded):
|
|
| 37 |
cypher_prompt = f"""
|
| 38 |
You are an expert Neo4j Cypher query generator.
|
| 39 |
|
| 40 |
-
Given a user’s question, graph schema, and **contextual flags**, generate the correct Cypher query. The query should
|
| 41 |
|
| 42 |
ONLY output the Cypher query. Do NOT explain anything.
|
| 43 |
|
|
@@ -49,6 +50,14 @@ def generate_cypher(user_query, query_intent, include_cobranded):
|
|
| 49 |
- (Feature): Properties = name
|
| 50 |
- Relationships:
|
| 51 |
- (Card)-[:HAS_FEATURE]->(Feature)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 52 |
|
| 53 |
Valid values:
|
| 54 |
- card_type: 'FD Card' or 'Regular'
|
|
@@ -58,31 +67,62 @@ def generate_cypher(user_query, query_intent, include_cobranded):
|
|
| 58 |
MANDATORY Condition Rules:
|
| 59 |
- If FD Card intent is true → include: `c.card_type = 'FD Card'`
|
| 60 |
- Else → include: `c.card_type = 'Regular'`
|
|
|
|
| 61 |
- If the query uses words like "premium", "elite", "luxury", "exclusive", "infinia", "black", etc. → include: `AND c.premium = true`
|
|
|
|
| 62 |
- If include co-branded is false → include: `AND (c.co_branded IS NULL OR c.co_branded = false)`
|
| 63 |
- These conditions are **MANDATORY**. If they apply, include them in the `WHERE` clause. Do not skip them.
|
| 64 |
|
| 65 |
---
|
| 66 |
|
| 67 |
Available features:
|
| 68 |
-
|
| 69 |
-
|
| 70 |
-
|
| 71 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 72 |
|
| 73 |
-
|
| 74 |
-
|
| 75 |
-
-
|
| 76 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 77 |
|
| 78 |
---
|
| 79 |
|
| 80 |
{context_note}
|
| 81 |
|
| 82 |
-
User Query: {user_query}
|
| 83 |
Cypher:
|
| 84 |
"""
|
| 85 |
|
|
|
|
|
|
|
| 86 |
response = model3.generate_content(cypher_prompt.strip())
|
| 87 |
cypher_code = response.text.strip()
|
| 88 |
|
|
@@ -95,7 +135,7 @@ def generate_cypher(user_query, query_intent, include_cobranded):
|
|
| 95 |
|
| 96 |
return cypher_code
|
| 97 |
|
| 98 |
-
# print(generate_cypher("
|
| 99 |
|
| 100 |
#generating embeddings (run only once)
|
| 101 |
def chunk_text(text, chunk_size=1):
|
|
@@ -114,7 +154,7 @@ def get_gemini_embeddings(text_list):
|
|
| 114 |
return np.vstack(embeddings)
|
| 115 |
|
| 116 |
# Loading credit card data
|
| 117 |
-
df = pd.read_csv("
|
| 118 |
card_descriptions = dict(zip(df["name"], df["description"]))
|
| 119 |
|
| 120 |
# Chunk all card descriptions
|
|
@@ -162,83 +202,7 @@ def eligibility_filter(cards, user_income, user_cibil, user_age,min_joining_fee,
|
|
| 162 |
|
| 163 |
return eligible_cards
|
| 164 |
|
| 165 |
-
|
| 166 |
-
def classify_user_query(user_query, labels):
|
| 167 |
-
genai.configure(api_key='AIzaSyAHoi9xbYAThtjXlyF_IKFtruoWYoUCjJQ')
|
| 168 |
-
model3 = genai.GenerativeModel('gemini-1.5-flash-latest')
|
| 169 |
-
print("classifying labels")
|
| 170 |
-
prompt = f"""
|
| 171 |
-
You are an assistant that classifies a user's credit card search query into predefined categories.
|
| 172 |
-
|
| 173 |
-
## Task:
|
| 174 |
-
Given the user query, select the most relevant labels from the list below. Return only labels that are clearly applicable.
|
| 175 |
-
|
| 176 |
-
## Label List:
|
| 177 |
-
{json.dumps(labels, indent=2)}
|
| 178 |
-
|
| 179 |
-
## Format:
|
| 180 |
-
Return only JSON like this:
|
| 181 |
-
{{ "labels": ["Label A", "Label B"] }}
|
| 182 |
-
|
| 183 |
-
## User Query:
|
| 184 |
-
"{user_query}"
|
| 185 |
-
"""
|
| 186 |
-
try:
|
| 187 |
-
response = model3.generate_content(prompt)
|
| 188 |
-
text = re.sub(r"^```(?:json)?\s*|\s*```$", "", response.text.strip(), flags=re.IGNORECASE)
|
| 189 |
-
return json.loads(text).get("labels", [])
|
| 190 |
-
except Exception as e:
|
| 191 |
-
print("Error classifying user query:", e)
|
| 192 |
-
return []
|
| 193 |
-
|
| 194 |
-
labels_list = [
|
| 195 |
-
"Best for International Travel",
|
| 196 |
-
"Best for Domestic Travel",
|
| 197 |
-
"Best for Vacation",
|
| 198 |
-
"Best for International Vacation",
|
| 199 |
-
"Best for Frequent Flyers",
|
| 200 |
-
"Low/No Forex Markup Fee",
|
| 201 |
-
"Best for Air Miles Accumulation",
|
| 202 |
-
"Best for flight tickets discount",
|
| 203 |
-
"Airport Lounge Access Priority",
|
| 204 |
-
"Railway Lounge Access",
|
| 205 |
-
"Hotel Benefits & Discounts",
|
| 206 |
-
"Hotel Loyalty Program Integration",
|
| 207 |
-
"Best for Dining Out / Fine Dining",
|
| 208 |
-
"Best for Online Food Ordering",
|
| 209 |
-
"Best for Bars, Pubs & Nightlife",
|
| 210 |
-
"Dining + Movie Combo Offers",
|
| 211 |
-
"Best for Lifestyle & Luxury Perks",
|
| 212 |
-
"Best for Online Shoppers",
|
| 213 |
-
"Best for Grocery Shopping",
|
| 214 |
-
"Best for Utility Bill Payments",
|
| 215 |
-
"Best for Fuel Spends",
|
| 216 |
-
"Fuel + Grocery Combo",
|
| 217 |
-
"Best for E-commerce Platforms",
|
| 218 |
-
"Best for Everyday Spends (Household)",
|
| 219 |
-
"Best for Cashback",
|
| 220 |
-
"Flat Cashback Cards",
|
| 221 |
-
"Best for Reward Points Accumulation",
|
| 222 |
-
"Accelerated Rewards for Select Categories",
|
| 223 |
-
"Rotating Category Reward Cards",
|
| 224 |
-
"Includes Travel Insurance",
|
| 225 |
-
"Includes Health / Life Insurance",
|
| 226 |
-
"Purchase Protection & Extended Warranty",
|
| 227 |
-
"Fraud Liability Coverage",
|
| 228 |
-
"Concierge & Lifestyle Management Services",
|
| 229 |
-
"Best Welcome Bonus",
|
| 230 |
-
"Lifetime Free Card",
|
| 231 |
-
"Low Annual Fee, High Value",
|
| 232 |
-
"No Annual Fee",
|
| 233 |
-
"Best for EMI Conversion",
|
| 234 |
-
"Best for Air Miles",
|
| 235 |
-
"Best for Reward Miles",
|
| 236 |
-
"Best for Movie Ticket Discounts",
|
| 237 |
-
"Best for Students",
|
| 238 |
-
"Best for Beginners / First-time Users",
|
| 239 |
-
"Best for Credit Score Building",
|
| 240 |
-
"Best for Low Credit Score Applicants"
|
| 241 |
-
]
|
| 242 |
|
| 243 |
#function for retrieving cards from knowledge graph
|
| 244 |
class Neo4jConnectionError(Exception):
|
|
@@ -249,19 +213,10 @@ def run_cypher_query(user_query, query, use_eligibility, user_income, user_cibil
|
|
| 249 |
|
| 250 |
try:
|
| 251 |
with driver.session() as session:
|
| 252 |
-
classified_labels = classify_user_query(user_query, labels_list)
|
| 253 |
-
print("Classified Labels:", classified_labels)
|
| 254 |
|
| 255 |
result = session.run(query)
|
| 256 |
matched_cards = [record["c"] for record in result]
|
| 257 |
-
|
| 258 |
-
if classified_labels:
|
| 259 |
-
filtered_cards = [
|
| 260 |
-
card["name"] for card in matched_cards
|
| 261 |
-
if any(label in card.get("labels", []) for label in classified_labels)
|
| 262 |
-
]
|
| 263 |
-
else:
|
| 264 |
-
filtered_cards = [card["name"] for card in matched_cards]
|
| 265 |
|
| 266 |
except Exception as e:
|
| 267 |
raise Neo4jConnectionError("Failed to connect to the Neo4j database.") from e
|
|
@@ -380,18 +335,19 @@ def convert_to_direct_query_gradio(user_query,preferences):
|
|
| 380 |
preferences_text = "User selected preferences: " + ", ".join(preferences) + "."
|
| 381 |
# Prompt with examples for indirect-to-direct conversion
|
| 382 |
prompt = f"""
|
| 383 |
-
You are an AI assistant that refines user queries to make them optimized for information retrieval
|
| 384 |
|
| 385 |
Instructions:
|
| 386 |
- Identify the main focus from the query.
|
| 387 |
- Reformat the query in a structured way for better retrieval.
|
| 388 |
- Ensure the most important feature appears first.
|
| 389 |
- Use precise keywords that match credit card benefits.
|
| 390 |
-
- Include the preferences also in the final query.
|
| 391 |
- Do NOT introduce new benefits not mentioned by the user.
|
| 392 |
-
- If the user
|
| 393 |
-
-
|
| 394 |
-
|
|
|
|
| 395 |
Examples:
|
| 396 |
|
| 397 |
Example 1
|
|
@@ -408,10 +364,10 @@ def convert_to_direct_query_gradio(user_query,preferences):
|
|
| 408 |
|
| 409 |
Now, optimize the following preferences and user query:
|
| 410 |
|
| 411 |
-
|
| 412 |
Preferences: "{preferences_text}"
|
| 413 |
User Query: "{user_query}"
|
| 414 |
"""
|
|
|
|
| 415 |
print("rewriting")
|
| 416 |
response = model2.generate_content(prompt)
|
| 417 |
|
|
@@ -530,7 +486,7 @@ def compare_selected_cards(selected_names, card_lookup):
|
|
| 530 |
return "<b style='color:red;'>Something went wrong while comparing. Please try again.</b>"
|
| 531 |
|
| 532 |
# Loading all 55 cards for comparison feature
|
| 533 |
-
df_all_cards = pd.read_csv("
|
| 534 |
all_card_names = df_all_cards["name"].tolist()
|
| 535 |
all_card_lookup = dict(zip(df_all_cards["name"], df_all_cards["description"]))
|
| 536 |
|
|
@@ -552,6 +508,7 @@ def find_intent(user_query: str) -> bool:
|
|
| 552 |
response = model2.generate_content(prompt)
|
| 553 |
result = response.text.strip().lower()
|
| 554 |
return result == "true"
|
|
|
|
| 555 |
|
| 556 |
|
| 557 |
#for queries enquiring about a card
|
|
@@ -576,23 +533,23 @@ def handle_query_classification(user_query):
|
|
| 576 |
genai.configure(api_key='AIzaSyDGc7l82E1sa4GarTfGiTXC6dLD7Crk8oo')
|
| 577 |
model1 = genai.GenerativeModel('gemini-1.5-flash-latest')
|
| 578 |
prompt = f"""
|
| 579 |
-
|
| 580 |
-
|
| 581 |
-
|
| 582 |
-
|
| 583 |
-
|
| 584 |
-
|
| 585 |
-
|
| 586 |
-
|
| 587 |
-
|
| 588 |
-
|
| 589 |
-
|
| 590 |
-
|
| 591 |
-
|
| 592 |
-
|
| 593 |
-
|
| 594 |
-
|
| 595 |
-
|
| 596 |
|
| 597 |
raw_response = model1.generate_content(prompt).text.strip()
|
| 598 |
|
|
@@ -610,6 +567,8 @@ def handle_query_classification(user_query):
|
|
| 610 |
print("Raw response from LLM:", raw_response)
|
| 611 |
raise
|
| 612 |
|
|
|
|
|
|
|
| 613 |
#passing the card mentioned in the user query
|
| 614 |
def find_matching_card(user_query):
|
| 615 |
lowered_query = user_query.lower()
|
|
@@ -617,7 +576,14 @@ def find_matching_card(user_query):
|
|
| 617 |
if row["name"].lower() in lowered_query:
|
| 618 |
return row.to_dict()
|
| 619 |
return None
|
|
|
|
|
|
|
|
|
|
| 620 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 621 |
|
| 622 |
#function to pass the retrieved cards and generated response to the UI
|
| 623 |
def recommend_cards_gradio(user_query, preferences, income, cibil, age,
|
|
@@ -710,18 +676,24 @@ def recommend_cards_gradio(user_query, preferences, income, cibil, age,
|
|
| 710 |
"No eligible card found"
|
| 711 |
)
|
| 712 |
|
| 713 |
-
|
| 714 |
-
|
| 715 |
-
for score, card in sorted(
|
| 716 |
-
zip(cross_encoder.predict([[direct_query, card["description"]] for card in cards]), cards),
|
| 717 |
-
reverse=True,
|
| 718 |
-
key=lambda x: x[0]
|
| 719 |
-
)
|
| 720 |
-
]
|
| 721 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 722 |
card_names = [row[0] for row in card_rows]
|
| 723 |
card_lookup = {row[0]: row[1] for row in card_rows}
|
| 724 |
-
|
| 725 |
top_card_html = f"""
|
| 726 |
<div style="
|
| 727 |
background-color: #fff3e0;
|
|
@@ -738,7 +710,7 @@ def recommend_cards_gradio(user_query, preferences, income, cibil, age,
|
|
| 738 |
</div>
|
| 739 |
"""
|
| 740 |
|
| 741 |
-
df_cards = pd.DataFrame(card_rows, columns=["Card Name", "Description"])
|
| 742 |
temp_file = tempfile.NamedTemporaryFile(delete=False, suffix=".csv")
|
| 743 |
df_cards.to_csv(temp_file.name, index=False)
|
| 744 |
|
|
@@ -754,6 +726,7 @@ def recommend_cards_gradio(user_query, preferences, income, cibil, age,
|
|
| 754 |
{},
|
| 755 |
"Unexpected error occurred, please try again in a while"
|
| 756 |
)
|
|
|
|
| 757 |
|
| 758 |
#function for the chatbot functionality
|
| 759 |
eligibility_lookup = {}
|
|
@@ -819,122 +792,120 @@ def chat_with_gemini(user_query, user_message, chat_history, card_lookup):
|
|
| 819 |
|
| 820 |
return chat_history, chat_history
|
| 821 |
|
| 822 |
-
#Interface
|
| 823 |
with gr.Blocks() as demo:
|
| 824 |
gr.Markdown("# Credit Card Recommender")
|
| 825 |
gr.Markdown("Get personalized credit card suggestions based on your lifestyle and eligibility.")
|
| 826 |
-
|
| 827 |
-
|
| 828 |
-
with gr.Row():
|
| 829 |
-
user_query = gr.Textbox(
|
| 830 |
-
label="Enter your query",
|
| 831 |
-
info="E.g., 'Best cards for international travel' or 'I want cashback cards with lounge access'"
|
| 832 |
-
)
|
| 833 |
-
preferences = gr.CheckboxGroup(
|
| 834 |
-
choices=["Cashback", "Travel Rewards", "Fuel Benefits", "International Lounge access",
|
| 835 |
-
"Domestic Lounge access", "Railway benefits", "Dining", "Shopping"],
|
| 836 |
-
label="Credit card categories:",
|
| 837 |
-
info="Select the features or benefits you want from your credit card"
|
| 838 |
-
)
|
| 839 |
|
| 840 |
-
|
| 841 |
-
|
| 842 |
-
|
| 843 |
-
|
| 844 |
-
|
| 845 |
-
|
| 846 |
-
|
| 847 |
-
|
| 848 |
-
|
| 849 |
-
|
| 850 |
-
|
| 851 |
-
|
| 852 |
-
age = gr.Slider(
|
| 853 |
-
minimum=18, maximum=75, step=1,
|
| 854 |
-
label="Age",
|
| 855 |
-
info="Some cards have minimum and maximum age eligibility"
|
| 856 |
-
)
|
| 857 |
-
|
| 858 |
-
with gr.Row():
|
| 859 |
-
min_joining_fee = gr.Number(
|
| 860 |
-
label="Min Joining Fee (₹)", value=0,
|
| 861 |
-
info="Minimum one-time fee to get the card"
|
| 862 |
-
)
|
| 863 |
-
max_joining_fee = gr.Number(
|
| 864 |
-
label="Max Joining Fee (₹)", value=150000,
|
| 865 |
-
info="Maximum one-time fee to get the card"
|
| 866 |
-
)
|
| 867 |
-
|
| 868 |
-
with gr.Row():
|
| 869 |
-
min_annual_fee = gr.Number(
|
| 870 |
-
label="Min Annual Fee (₹)", value=0,
|
| 871 |
-
info="Minimum yearly fee to be paid"
|
| 872 |
-
)
|
| 873 |
-
max_annual_fee = gr.Number(
|
| 874 |
-
label="Max Annual Fee (₹)", value=150000,
|
| 875 |
-
info="Maximum yearly fee to be paid"
|
| 876 |
-
)
|
| 877 |
|
| 878 |
-
|
| 879 |
-
|
| 880 |
-
|
| 881 |
-
|
| 882 |
-
|
| 883 |
-
|
| 884 |
-
|
| 885 |
-
|
| 886 |
-
|
| 887 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 888 |
|
| 889 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 890 |
|
| 891 |
-
|
| 892 |
-
|
| 893 |
-
|
| 894 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 895 |
|
| 896 |
-
# States for recommendations and chatbot
|
| 897 |
card_names_state = gr.State()
|
| 898 |
card_lookup_state = gr.State()
|
| 899 |
chat_history = gr.State([])
|
| 900 |
-
query=gr.State([])
|
| 901 |
-
|
| 902 |
-
# Recommended cards comparison
|
| 903 |
-
gr.Markdown("### Compare Recommended Cards")
|
| 904 |
-
compare_checkboxes = gr.CheckboxGroup(
|
| 905 |
-
choices=[], label="Select 2 or more cards to compare",
|
| 906 |
-
info="Pick 2+ cards from the recommended list to see a comparison"
|
| 907 |
-
)
|
| 908 |
-
compare_btn = gr.Button("Compare Selected Cards", variant='primary')
|
| 909 |
-
compare_output = gr.HTML()
|
| 910 |
-
|
| 911 |
-
# Full card list comparison
|
| 912 |
-
gr.Markdown("### Compare Any Cards from Full List")
|
| 913 |
-
full_compare_dropdown = gr.Dropdown(
|
| 914 |
-
choices=all_card_names, multiselect=True, label="Select any 2+ cards",
|
| 915 |
-
info="Manually compare any cards from the full database"
|
| 916 |
-
)
|
| 917 |
-
full_compare_btn = gr.Button("Compare Selected Cards", variant='primary')
|
| 918 |
-
full_compare_output = gr.HTML()
|
| 919 |
-
|
| 920 |
-
# Chatbot UI for follow-up questions
|
| 921 |
-
gr.Markdown("### Ask any follow-up question ")
|
| 922 |
-
chatbot = gr.Chatbot(type='messages')
|
| 923 |
|
| 924 |
-
# Function for passing inputs and generating the results
|
| 925 |
def wrapped_recommend_cards(user_query, preferences, income, cibil, age, min_joining_fee, max_joining_fee,
|
| 926 |
min_annual_fee, max_annual_fee, use_eligibility,include_cobranded):
|
| 927 |
top_html, df, file, card_names, card_lookup, direct_query = recommend_cards_gradio(
|
| 928 |
user_query, preferences, income, cibil, age, min_joining_fee, max_joining_fee,
|
| 929 |
min_annual_fee, max_annual_fee, use_eligibility,include_cobranded
|
| 930 |
-
|
| 931 |
-
|
| 932 |
df_label = f"Found {len(card_names)} cards"
|
| 933 |
-
|
| 934 |
return top_html, gr.update(value=df, label=df_label), file, card_names, card_lookup, gr.update(choices=card_names, value=[]), direct_query
|
| 935 |
|
| 936 |
-
|
| 937 |
-
# Button actions and UI interactions
|
| 938 |
submit_btn.click(
|
| 939 |
fn=wrapped_recommend_cards,
|
| 940 |
inputs=[user_query, preferences, income, cibil, age, min_joining_fee, max_joining_fee,
|
|
@@ -954,17 +925,11 @@ with gr.Blocks() as demo:
|
|
| 954 |
outputs=full_compare_output
|
| 955 |
)
|
| 956 |
|
| 957 |
-
# Chatbot function for handling follow-up queries
|
| 958 |
-
user_query_for_chat = gr.Textbox(
|
| 959 |
-
label="Enter your question",
|
| 960 |
-
info="Ask follow-ups like 'Which card has better travel insurance?' or 'Which card has less annual fee'"
|
| 961 |
-
)
|
| 962 |
-
submit_query_btn = gr.Button("Submit Query", variant='primary')
|
| 963 |
-
|
| 964 |
submit_query_btn.click(
|
| 965 |
fn=chat_with_gemini,
|
| 966 |
inputs=[query,user_query_for_chat, chat_history, card_lookup_state],
|
| 967 |
outputs=[chatbot, chat_history]
|
| 968 |
)
|
| 969 |
|
|
|
|
| 970 |
demo.launch(share=True)
|
|
|
|
| 10 |
import gradio as gr
|
| 11 |
import tempfile
|
| 12 |
import re
|
| 13 |
+
import ast
|
| 14 |
|
| 15 |
#for adding bank name to the cards in the graph
|
| 16 |
+
eligibility_df = pd.read_csv("cards_eligibility_updated.csv")
|
| 17 |
card_to_bank = dict(zip(eligibility_df['Name'], eligibility_df['Bank']))
|
| 18 |
|
| 19 |
#neo4j credentials
|
|
|
|
| 38 |
cypher_prompt = f"""
|
| 39 |
You are an expert Neo4j Cypher query generator.
|
| 40 |
|
| 41 |
+
Given a user’s question, graph schema, and **contextual flags**, generate the correct Cypher query. The query should return only the cards `c`.
|
| 42 |
|
| 43 |
ONLY output the Cypher query. Do NOT explain anything.
|
| 44 |
|
|
|
|
| 50 |
- (Feature): Properties = name
|
| 51 |
- Relationships:
|
| 52 |
- (Card)-[:HAS_FEATURE]->(Feature)
|
| 53 |
+
|
| 54 |
+
Feature Inclusion Rules:
|
| 55 |
+
- Only include relevant features based on user query.
|
| 56 |
+
- Forex markup fee and foreign transaction fee are the same.
|
| 57 |
+
- Don’t add “General Cashback” or “General Reward Points” unless explicitly mentioned.
|
| 58 |
+
- If fuel is mentioned, include both `Fuel Benefits` and `Fuel Surcharge Waiver`.
|
| 59 |
+
- **ALWAYS** match features using: `f.name IN [...]` — even if there is only **one** feature.
|
| 60 |
+
|
| 61 |
|
| 62 |
Valid values:
|
| 63 |
- card_type: 'FD Card' or 'Regular'
|
|
|
|
| 67 |
MANDATORY Condition Rules:
|
| 68 |
- If FD Card intent is true → include: `c.card_type = 'FD Card'`
|
| 69 |
- Else → include: `c.card_type = 'Regular'`
|
| 70 |
+
- If the query is based on beginners or students or people with no or low credit history then use FD Card.
|
| 71 |
- If the query uses words like "premium", "elite", "luxury", "exclusive", "infinia", "black", etc. → include: `AND c.premium = true`
|
| 72 |
+
- If the query includes low spending, without high spending or budget → include: `(c.premium IS NULL OR c.premium = false)`
|
| 73 |
- If include co-branded is false → include: `AND (c.co_branded IS NULL OR c.co_branded = false)`
|
| 74 |
- These conditions are **MANDATORY**. If they apply, include them in the `WHERE` clause. Do not skip them.
|
| 75 |
|
| 76 |
---
|
| 77 |
|
| 78 |
Available features:
|
| 79 |
+
"General Cashback", "Fuel Surcharge Waiver", "Fuel Benefits", "Welcome Bonus",
|
| 80 |
+
"Airport Lounge Access", "General Reward Points", "Domestic Travel Benefits",
|
| 81 |
+
"Movie Benefits", "Flight Discounts", "International Travel Benefits",
|
| 82 |
+
"Hotel Benefits", "Dining Benefits", "Daily Spends (Grocery)", "Railway Benefits",
|
| 83 |
+
"Travel Benefits", "Railway Lounge", "Insurance", "Utility",
|
| 84 |
+
"E-commerce Platform Benefits", "Air Miles", "Spa Access Benefits",
|
| 85 |
+
"Lifestyle & Luxury Perks", "Golf Access & Perks", "Online Shopping Benefits",
|
| 86 |
+
"UPI Transaction Support", "Health Benefits", "EMI Conversion Options",
|
| 87 |
+
"No Forex Markup Fee", "Roadside Assistance", "Rupay Network Support"
|
| 88 |
|
| 89 |
+
---
|
| 90 |
+
|
| 91 |
+
Few-shot Examples:
|
| 92 |
+
|
| 93 |
+
User Query: Show premium cards with airport lounge access
|
| 94 |
+
Cypher:
|
| 95 |
+
MATCH (c:Card)-[:HAS_FEATURE]->(f:Feature)
|
| 96 |
+
WHERE f.name IN ["Airport Lounge Access"]
|
| 97 |
+
AND c.card_type = 'Regular'
|
| 98 |
+
AND c.premium = true
|
| 99 |
+
RETURN c
|
| 100 |
+
|
| 101 |
+
User Query: I want FD cards with spa access and golf perks
|
| 102 |
+
Cypher:
|
| 103 |
+
MATCH (c:Card)-[:HAS_FEATURE]->(f:Feature)
|
| 104 |
+
WHERE f.name IN ["Spa Access Benefits", "Golf Access & Perks"]
|
| 105 |
+
AND c.card_type = 'FD Card'
|
| 106 |
+
RETURN c
|
| 107 |
+
|
| 108 |
+
User Query: Cards that support UPI but are not co-branded
|
| 109 |
+
Cypher:
|
| 110 |
+
MATCH (c:Card)-[:HAS_FEATURE]->(f:Feature)
|
| 111 |
+
WHERE f.name IN ["UPI Transaction Support"]
|
| 112 |
+
AND c.card_type = 'Regular'
|
| 113 |
+
AND (c.co_branded IS NULL OR c.co_branded = false)
|
| 114 |
+
RETURN c
|
| 115 |
|
| 116 |
---
|
| 117 |
|
| 118 |
{context_note}
|
| 119 |
|
| 120 |
+
User Query: {user_query}
|
| 121 |
Cypher:
|
| 122 |
"""
|
| 123 |
|
| 124 |
+
|
| 125 |
+
|
| 126 |
response = model3.generate_content(cypher_prompt.strip())
|
| 127 |
cypher_code = response.text.strip()
|
| 128 |
|
|
|
|
| 135 |
|
| 136 |
return cypher_code
|
| 137 |
|
| 138 |
+
# print(generate_cypher("I shop a lot, both online and in stores, and I want a credit card that gives me good cashback on my purchases. Which card would be the best for saving money? I need a budget cxard", True,False))
|
| 139 |
|
| 140 |
#generating embeddings (run only once)
|
| 141 |
def chunk_text(text, chunk_size=1):
|
|
|
|
| 154 |
return np.vstack(embeddings)
|
| 155 |
|
| 156 |
# Loading credit card data
|
| 157 |
+
df = pd.read_csv("credit_card_data_updated.csv")
|
| 158 |
card_descriptions = dict(zip(df["name"], df["description"]))
|
| 159 |
|
| 160 |
# Chunk all card descriptions
|
|
|
|
| 202 |
|
| 203 |
return eligible_cards
|
| 204 |
|
| 205 |
+
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 206 |
|
| 207 |
#function for retrieving cards from knowledge graph
|
| 208 |
class Neo4jConnectionError(Exception):
|
|
|
|
| 213 |
|
| 214 |
try:
|
| 215 |
with driver.session() as session:
|
|
|
|
|
|
|
| 216 |
|
| 217 |
result = session.run(query)
|
| 218 |
matched_cards = [record["c"] for record in result]
|
| 219 |
+
filtered_cards = [card["name"] for card in matched_cards]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 220 |
|
| 221 |
except Exception as e:
|
| 222 |
raise Neo4jConnectionError("Failed to connect to the Neo4j database.") from e
|
|
|
|
| 335 |
preferences_text = "User selected preferences: " + ", ".join(preferences) + "."
|
| 336 |
# Prompt with examples for indirect-to-direct conversion
|
| 337 |
prompt = f"""
|
| 338 |
+
You are an AI assistant that refines user queries to make them optimized for information retrieval while keeping the original intent.
|
| 339 |
|
| 340 |
Instructions:
|
| 341 |
- Identify the main focus from the query.
|
| 342 |
- Reformat the query in a structured way for better retrieval.
|
| 343 |
- Ensure the most important feature appears first.
|
| 344 |
- Use precise keywords that match credit card benefits.
|
| 345 |
+
- Include the preferences also in the final query.
|
| 346 |
- Do NOT introduce new benefits not mentioned by the user.
|
| 347 |
+
- If the user uses vague terms like "vacation", interpret it as travel-related benefits including: airport lounge access, international/domestic travel, hotel benefits, forex waiver.
|
| 348 |
+
- If the query includes terms like "beginner", "entry-level", or "low credit score", include essential features such as cashback, reward points, and basic offers.
|
| 349 |
+
- If the query is empty, generate a useful retrieval-focused query based solely on preferences.
|
| 350 |
+
|
| 351 |
Examples:
|
| 352 |
|
| 353 |
Example 1
|
|
|
|
| 364 |
|
| 365 |
Now, optimize the following preferences and user query:
|
| 366 |
|
|
|
|
| 367 |
Preferences: "{preferences_text}"
|
| 368 |
User Query: "{user_query}"
|
| 369 |
"""
|
| 370 |
+
|
| 371 |
print("rewriting")
|
| 372 |
response = model2.generate_content(prompt)
|
| 373 |
|
|
|
|
| 486 |
return "<b style='color:red;'>Something went wrong while comparing. Please try again.</b>"
|
| 487 |
|
| 488 |
# Loading all 55 cards for comparison feature
|
| 489 |
+
df_all_cards = pd.read_csv("credit_card_data_updated.csv")
|
| 490 |
all_card_names = df_all_cards["name"].tolist()
|
| 491 |
all_card_lookup = dict(zip(df_all_cards["name"], df_all_cards["description"]))
|
| 492 |
|
|
|
|
| 508 |
response = model2.generate_content(prompt)
|
| 509 |
result = response.text.strip().lower()
|
| 510 |
return result == "true"
|
| 511 |
+
# print(find_intent("im a beginner to credit cards suggest me suitable cards"))
|
| 512 |
|
| 513 |
|
| 514 |
#for queries enquiring about a card
|
|
|
|
| 533 |
genai.configure(api_key='AIzaSyDGc7l82E1sa4GarTfGiTXC6dLD7Crk8oo')
|
| 534 |
model1 = genai.GenerativeModel('gemini-1.5-flash-latest')
|
| 535 |
prompt = f"""
|
| 536 |
+
You are a smart financial assistant.
|
| 537 |
+
|
| 538 |
+
### User's Query:
|
| 539 |
+
{user_query}
|
| 540 |
+
|
| 541 |
+
### Task:
|
| 542 |
+
Classify the user's intent into one of the following categories:
|
| 543 |
+
1. "retrieve" → If the user is asking for card suggestions, recommendations, or showing cards (e.g., "suggest a card", "need a travel card").
|
| 544 |
+
2. "specific" → If the user is asking about a particular card by name, the card names need not end with "card" keyword, so also consider that (e.g., "Tell me about HDFC Regalia", "Is SBI Elite good?").
|
| 545 |
+
3. "no_retrieval" → If the query is generic, chit-chat, or doesn't need credit card data lookup (e.g., "What is interest in credit cards?").
|
| 546 |
+
|
| 547 |
+
Respond in the following JSON format:
|
| 548 |
+
{{
|
| 549 |
+
"intent": "retrieve" | "specific" | "no_retrieval",
|
| 550 |
+
"response": "Only include this if intent is 'no_retrieval'"
|
| 551 |
+
}}
|
| 552 |
+
"""
|
| 553 |
|
| 554 |
raw_response = model1.generate_content(prompt).text.strip()
|
| 555 |
|
|
|
|
| 567 |
print("Raw response from LLM:", raw_response)
|
| 568 |
raise
|
| 569 |
|
| 570 |
+
# print(handle_query_classification("what is a credit card"))
|
| 571 |
+
|
| 572 |
#passing the card mentioned in the user query
|
| 573 |
def find_matching_card(user_query):
|
| 574 |
lowered_query = user_query.lower()
|
|
|
|
| 576 |
if row["name"].lower() in lowered_query:
|
| 577 |
return row.to_dict()
|
| 578 |
return None
|
| 579 |
+
|
| 580 |
+
with open('for_graph_construction_(expanded labels).json') as f:
|
| 581 |
+
card_feature_data = json.load(f)
|
| 582 |
|
| 583 |
+
card_features_lookup = {
|
| 584 |
+
card['card_name']: set(card['features'])
|
| 585 |
+
for card in card_feature_data
|
| 586 |
+
}
|
| 587 |
|
| 588 |
#function to pass the retrieved cards and generated response to the UI
|
| 589 |
def recommend_cards_gradio(user_query, preferences, income, cibil, age,
|
|
|
|
| 676 |
"No eligible card found"
|
| 677 |
)
|
| 678 |
|
| 679 |
+
match = re.search(r"f\.name IN (\[.*?\])", cypher_query)
|
| 680 |
+
query_features = set(ast.literal_eval(match.group(1))) if match else set()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 681 |
|
| 682 |
+
card_rows = []
|
| 683 |
+
for score, card in sorted(
|
| 684 |
+
zip(cross_encoder.predict([[direct_query, card["description"]] for card in cards]), cards),
|
| 685 |
+
reverse=True,
|
| 686 |
+
key=lambda x: x[0]
|
| 687 |
+
):
|
| 688 |
+
card_name = card["name"]
|
| 689 |
+
card_desc = card["description"]
|
| 690 |
+
matched_features = query_features.intersection(card_features_lookup.get(card_name, set()))
|
| 691 |
+
feature_str = ", ".join(matched_features) if matched_features else "None"
|
| 692 |
+
card_rows.append([card_name, feature_str, card_desc])
|
| 693 |
+
|
| 694 |
+
|
| 695 |
card_names = [row[0] for row in card_rows]
|
| 696 |
card_lookup = {row[0]: row[1] for row in card_rows}
|
|
|
|
| 697 |
top_card_html = f"""
|
| 698 |
<div style="
|
| 699 |
background-color: #fff3e0;
|
|
|
|
| 710 |
</div>
|
| 711 |
"""
|
| 712 |
|
| 713 |
+
df_cards = pd.DataFrame(card_rows, columns=["Card Name", "Matched Features", "Description"])
|
| 714 |
temp_file = tempfile.NamedTemporaryFile(delete=False, suffix=".csv")
|
| 715 |
df_cards.to_csv(temp_file.name, index=False)
|
| 716 |
|
|
|
|
| 726 |
{},
|
| 727 |
"Unexpected error occurred, please try again in a while"
|
| 728 |
)
|
| 729 |
+
|
| 730 |
|
| 731 |
#function for the chatbot functionality
|
| 732 |
eligibility_lookup = {}
|
|
|
|
| 792 |
|
| 793 |
return chat_history, chat_history
|
| 794 |
|
| 795 |
+
# Interface with Tabs
|
| 796 |
with gr.Blocks() as demo:
|
| 797 |
gr.Markdown("# Credit Card Recommender")
|
| 798 |
gr.Markdown("Get personalized credit card suggestions based on your lifestyle and eligibility.")
|
| 799 |
+
|
| 800 |
+
with gr.Tabs():
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 801 |
|
| 802 |
+
with gr.Tab("🧠 Get Recommendations"):
|
| 803 |
+
with gr.Row():
|
| 804 |
+
user_query = gr.Textbox(
|
| 805 |
+
label="Enter your query",
|
| 806 |
+
info="E.g., 'Best cards for international travel' or 'I want cashback cards with lounge access'"
|
| 807 |
+
)
|
| 808 |
+
preferences = gr.CheckboxGroup(
|
| 809 |
+
choices=["Cashback", "Travel Rewards", "Fuel Benefits", "International Lounge access",
|
| 810 |
+
"Domestic Lounge access", "Railway benefits", "Dining", "Shopping"],
|
| 811 |
+
label="Credit card categories:",
|
| 812 |
+
info="Select the features or benefits you want from your credit card"
|
| 813 |
+
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 814 |
|
| 815 |
+
with gr.Accordion("Eligibility filters menu", open=False):
|
| 816 |
+
with gr.Row():
|
| 817 |
+
income = gr.Slider(
|
| 818 |
+
minimum=1, maximum=60, step=1,
|
| 819 |
+
label="Annual Income (LPA) Minimum requirement is 2.5",
|
| 820 |
+
info="Helps filter cards based on your income eligibility (in Lakhs Per Annum)"
|
| 821 |
+
)
|
| 822 |
+
cibil = gr.Slider(
|
| 823 |
+
minimum=300, maximum=900, step=10,
|
| 824 |
+
label="CIBIL Score",
|
| 825 |
+
info="Most of the cards requires a credit score of 700+"
|
| 826 |
+
)
|
| 827 |
+
age = gr.Slider(
|
| 828 |
+
minimum=18, maximum=75, step=1,
|
| 829 |
+
label="Age",
|
| 830 |
+
info="Some cards have minimum and maximum age eligibility"
|
| 831 |
+
)
|
| 832 |
|
| 833 |
+
with gr.Row():
|
| 834 |
+
min_joining_fee = gr.Number(
|
| 835 |
+
label="Min Joining Fee (₹)", value=0,
|
| 836 |
+
info="Minimum one-time fee to get the card"
|
| 837 |
+
)
|
| 838 |
+
max_joining_fee = gr.Number(
|
| 839 |
+
label="Max Joining Fee (₹)", value=150000,
|
| 840 |
+
info="Maximum one-time fee to get the card"
|
| 841 |
+
)
|
| 842 |
|
| 843 |
+
with gr.Row():
|
| 844 |
+
min_annual_fee = gr.Number(
|
| 845 |
+
label="Min Annual Fee (₹)", value=0,
|
| 846 |
+
info="Minimum yearly fee to be paid"
|
| 847 |
+
)
|
| 848 |
+
max_annual_fee = gr.Number(
|
| 849 |
+
label="Max Annual Fee (₹)", value=150000,
|
| 850 |
+
info="Maximum yearly fee to be paid"
|
| 851 |
+
)
|
| 852 |
+
|
| 853 |
+
with gr.Row():
|
| 854 |
+
use_eligibility = gr.Checkbox(
|
| 855 |
+
label="Apply Eligibility Filter", value=False,
|
| 856 |
+
info="Enable this to get recommendations of the cards only for which you are eligible for"
|
| 857 |
+
)
|
| 858 |
+
include_cobranded = gr.Checkbox(
|
| 859 |
+
label="Include Co-branded Cards", value=True,
|
| 860 |
+
info="Include cards that are co-branded with airlines, retailers, etc."
|
| 861 |
+
)
|
| 862 |
+
|
| 863 |
+
submit_btn = gr.Button("Recommend Cards", variant='primary')
|
| 864 |
+
|
| 865 |
+
top_card_html = gr.HTML()
|
| 866 |
+
card_df = gr.Dataframe(headers=["Card Name", "Matched Features", "Description"])
|
| 867 |
+
card_file = gr.File(label="Download Full Recommendations (CSV)")
|
| 868 |
+
|
| 869 |
+
with gr.Tab("🔍 Compare Cards"):
|
| 870 |
+
gr.Markdown("### Compare Recommended Cards")
|
| 871 |
+
compare_checkboxes = gr.CheckboxGroup(
|
| 872 |
+
choices=[], label="Select 2 or more cards to compare",
|
| 873 |
+
info="Pick 2+ cards from the recommended list to see a comparison"
|
| 874 |
+
)
|
| 875 |
+
compare_btn = gr.Button("Compare Selected Cards", variant='primary')
|
| 876 |
+
compare_output = gr.HTML()
|
| 877 |
+
|
| 878 |
+
gr.Markdown("### Compare Any Cards from Full List")
|
| 879 |
+
full_compare_dropdown = gr.Dropdown(
|
| 880 |
+
choices=all_card_names, multiselect=True, label="Select any 2+ cards",
|
| 881 |
+
info="Manually compare any cards from the full database"
|
| 882 |
+
)
|
| 883 |
+
full_compare_btn = gr.Button("Compare Selected Cards", variant='primary')
|
| 884 |
+
full_compare_output = gr.HTML()
|
| 885 |
+
|
| 886 |
+
with gr.Tab("💬 Ask Follow-up Questions"):
|
| 887 |
+
gr.Markdown("### Ask any follow-up question ")
|
| 888 |
+
chatbot = gr.Chatbot(type='messages')
|
| 889 |
+
user_query_for_chat = gr.Textbox(
|
| 890 |
+
label="Enter your question",
|
| 891 |
+
info="Ask follow-ups like 'Which card has better travel insurance?' or 'Which card has less annual fee'"
|
| 892 |
+
)
|
| 893 |
+
submit_query_btn = gr.Button("Submit Query", variant='primary')
|
| 894 |
|
|
|
|
| 895 |
card_names_state = gr.State()
|
| 896 |
card_lookup_state = gr.State()
|
| 897 |
chat_history = gr.State([])
|
| 898 |
+
query = gr.State([])
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 899 |
|
|
|
|
| 900 |
def wrapped_recommend_cards(user_query, preferences, income, cibil, age, min_joining_fee, max_joining_fee,
|
| 901 |
min_annual_fee, max_annual_fee, use_eligibility,include_cobranded):
|
| 902 |
top_html, df, file, card_names, card_lookup, direct_query = recommend_cards_gradio(
|
| 903 |
user_query, preferences, income, cibil, age, min_joining_fee, max_joining_fee,
|
| 904 |
min_annual_fee, max_annual_fee, use_eligibility,include_cobranded
|
| 905 |
+
)
|
|
|
|
| 906 |
df_label = f"Found {len(card_names)} cards"
|
|
|
|
| 907 |
return top_html, gr.update(value=df, label=df_label), file, card_names, card_lookup, gr.update(choices=card_names, value=[]), direct_query
|
| 908 |
|
|
|
|
|
|
|
| 909 |
submit_btn.click(
|
| 910 |
fn=wrapped_recommend_cards,
|
| 911 |
inputs=[user_query, preferences, income, cibil, age, min_joining_fee, max_joining_fee,
|
|
|
|
| 925 |
outputs=full_compare_output
|
| 926 |
)
|
| 927 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 928 |
submit_query_btn.click(
|
| 929 |
fn=chat_with_gemini,
|
| 930 |
inputs=[query,user_query_for_chat, chat_history, card_lookup_state],
|
| 931 |
outputs=[chatbot, chat_history]
|
| 932 |
)
|
| 933 |
|
| 934 |
+
|
| 935 |
demo.launch(share=True)
|